Determine D3D texture storage size with correct base level

The size of the texture storage is now determined by extrapolating
the level zero texture dimensions from the base level dimensions.
This fixes crashing when images for levels below the base level are
not defined, and also fixes texture storage dimensions being
calculated wrong in case the levels outside the used level range have
dimensions that are inconsistent with the dimensions inside the used
level range.

Checking texture level completeness in TextureD3D is now done based on
the dimensions of the base level, and levels that are outside the base
level to max level range are not taken into account. Textures are
marked incomplete in case their base level is greater than their max
level.

Changing the base level can also affect the size of the storage
required for the texture. Old storage is now discarded when the base
level is changed and the new base level calls for different storage
dimensions.

Code in TextureD3D is refactored so that "base level" actually means
the base level of the texture specified through the GLES API, and
"level zero" is used where TextureD3D would sometimes previously use
"base level".

Changing either the base level or max level can also affect texture
completeness, so invalidate the cached completeness in Texture if
they are changed.

Some of the added tests are still failing on Intel and NVIDIA OpenGL
drivers because of driver bugs. Tests also fail on OSX.

BUG=angleproject:596
TEST=angle_end2end_tests,
     dEQP-GLES3.functional.texture.* (no regressions),
     dEQP-GLES3.functional.shaders.texture_functions.* (no regressions),
     dEQP-GLES3.functional.state_query.texture.* (no regressions)

Change-Id: Icd73d6e29f84a341ed5ff36d5ec5cb2f469cb4e8
Reviewed-on: https://chromium-review.googlesource.com/333352
Reviewed-by: Jamie Madill <jmadill@chromium.org>
Commit-Queue: Olli Etuaho <oetuaho@nvidia.com>
diff --git a/src/libANGLE/Texture.cpp b/src/libANGLE/Texture.cpp
index faa7012..3822e83 100644
--- a/src/libANGLE/Texture.cpp
+++ b/src/libANGLE/Texture.cpp
@@ -54,8 +54,10 @@
       mTexture(factory->createTexture(target)),
       mLabel(),
       mTextureState(),
+      mEffectiveBaseLevel(0),
       mTarget(target),
-      mImageDescs(IMPLEMENTATION_MAX_TEXTURE_LEVELS * (target == GL_TEXTURE_CUBE_MAP ? 6 : 1)),
+      mImageDescs((IMPLEMENTATION_MAX_TEXTURE_LEVELS + 1) *
+                  (target == GL_TEXTURE_CUBE_MAP ? 6 : 1)),
       mCompletenessCache(),
       mBoundSurface(nullptr),
       mBoundStream(nullptr)
@@ -239,7 +241,30 @@
 
 void Texture::setBaseLevel(GLuint baseLevel)
 {
-    mTextureState.baseLevel = baseLevel;
+    if (mTextureState.baseLevel != baseLevel)
+    {
+        mTextureState.baseLevel       = baseLevel;
+        mCompletenessCache.cacheValid = false;
+        updateEffectiveBaseLevel();
+        mTexture->setBaseLevel(mEffectiveBaseLevel);
+    }
+}
+
+void Texture::updateEffectiveBaseLevel()
+{
+    mEffectiveBaseLevel = mTextureState.baseLevel;
+    if (mTextureState.immutableFormat && mEffectiveBaseLevel > mTextureState.immutableLevels - 1)
+    {
+        mEffectiveBaseLevel = mTextureState.immutableLevels - 1;
+    }
+    // Ensure that this class doesn't access out-of-range memory when querying effective base level
+    // properties.
+    if (mEffectiveBaseLevel > gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS)
+    {
+        // gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS is still an out-of-range level, but the arrays
+        // for level data have an extra dummy level for querying out-of-range base level properties.
+        mEffectiveBaseLevel = gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS;
+    }
 }
 
 GLuint Texture::getBaseLevel() const
@@ -247,9 +272,18 @@
     return mTextureState.baseLevel;
 }
 
+GLuint Texture::getEffectiveBaseLevel() const
+{
+    return mEffectiveBaseLevel;
+}
+
 void Texture::setMaxLevel(GLuint maxLevel)
 {
-    mTextureState.maxLevel = maxLevel;
+    if (mTextureState.maxLevel != maxLevel)
+    {
+        mTextureState.maxLevel        = maxLevel;
+        mCompletenessCache.cacheValid = false;
+    }
 }
 
 GLuint Texture::getMaxLevel() const
@@ -309,7 +343,7 @@
 
 bool Texture::isSamplerComplete(const SamplerState &samplerState, const ContextState &data) const
 {
-    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mTextureState.baseLevel);
+    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mEffectiveBaseLevel);
     const TextureCaps &textureCaps = data.textureCaps->get(baseImageDesc.internalFormat);
     if (!mCompletenessCache.cacheValid ||
         mCompletenessCache.samplerState != samplerState ||
@@ -506,6 +540,7 @@
 
     mTextureState.immutableFormat = true;
     mTextureState.immutableLevels = static_cast<GLuint>(levels);
+    updateEffectiveBaseLevel();
     clearImageDescs();
     setImageDescChain(levels, size, internalFormat);
 
@@ -715,11 +750,19 @@
 bool Texture::computeSamplerCompleteness(const SamplerState &samplerState,
                                          const ContextState &data) const
 {
-    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mTextureState.baseLevel);
+    if (mTextureState.baseLevel > mTextureState.maxLevel)
+    {
+        return false;
+    }
+    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mEffectiveBaseLevel);
     if (baseImageDesc.size.width == 0 || baseImageDesc.size.height == 0 || baseImageDesc.size.depth == 0)
     {
         return false;
     }
+    // The cases where the texture is incomplete because base level is out of range should be
+    // handled by the above condition.
+    ASSERT(mTextureState.baseLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS ||
+           mTextureState.immutableFormat);
 
     if (mTarget == GL_TEXTURE_CUBE_MAP && baseImageDesc.size.width != baseImageDesc.size.height)
     {
@@ -792,7 +835,7 @@
 
     size_t maxLevel = std::min<size_t>(expectedMipLevels, mTextureState.maxLevel + 1);
 
-    for (size_t level = mTextureState.baseLevel; level < maxLevel; level++)
+    for (size_t level = mEffectiveBaseLevel; level < maxLevel; level++)
     {
         if (mTarget == GL_TEXTURE_CUBE_MAP)
         {
@@ -825,7 +868,7 @@
         return true;
     }
 
-    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mTextureState.baseLevel);
+    const ImageDesc &baseImageDesc = getImageDesc(getBaseImageTarget(), mEffectiveBaseLevel);
     if (baseImageDesc.size.width == 0 || baseImageDesc.size.height == 0 || baseImageDesc.size.depth == 0)
     {
         return false;
@@ -843,8 +886,8 @@
         return false;
     }
 
-    ASSERT(level >= mTextureState.baseLevel);
-    const size_t relativeLevel = level - mTextureState.baseLevel;
+    ASSERT(level >= mEffectiveBaseLevel);
+    const size_t relativeLevel = level - mEffectiveBaseLevel;
     if (levelImageDesc.size.width != std::max(1, baseImageDesc.size.width >> relativeLevel))
     {
         return false;
diff --git a/src/libANGLE/Texture.h b/src/libANGLE/Texture.h
index 4c3bed5..ebcd9a3 100644
--- a/src/libANGLE/Texture.h
+++ b/src/libANGLE/Texture.h
@@ -101,6 +101,8 @@
 
     void setBaseLevel(GLuint baseLevel);
     GLuint getBaseLevel() const;
+    // Returns base level after clamping required for immutable textures.
+    GLuint getEffectiveBaseLevel() const;
 
     void setMaxLevel(GLuint maxLevel);
     GLuint getMaxLevel() const;
@@ -207,6 +209,7 @@
     std::string mLabel;
 
     TextureState mTextureState;
+    GLuint mEffectiveBaseLevel;
 
     GLenum mTarget;
 
@@ -233,6 +236,8 @@
     void clearImageDescs();
     void releaseTexImageInternal();
 
+    void updateEffectiveBaseLevel();
+
     std::vector<ImageDesc> mImageDescs;
 
     struct SamplerCompletenessCache
diff --git a/src/libANGLE/renderer/TextureImpl.h b/src/libANGLE/renderer/TextureImpl.h
index db530a1..040265a 100644
--- a/src/libANGLE/renderer/TextureImpl.h
+++ b/src/libANGLE/renderer/TextureImpl.h
@@ -71,6 +71,8 @@
 
     virtual gl::Error generateMipmaps(const gl::TextureState &textureState) = 0;
 
+    virtual void setBaseLevel(GLuint baseLevel) = 0;
+
     virtual void bindTexImage(egl::Surface *surface) = 0;
     virtual void releaseTexImage() = 0;
 };
diff --git a/src/libANGLE/renderer/TextureImpl_mock.h b/src/libANGLE/renderer/TextureImpl_mock.h
index 7526a04..950c389 100644
--- a/src/libANGLE/renderer/TextureImpl_mock.h
+++ b/src/libANGLE/renderer/TextureImpl_mock.h
@@ -37,6 +37,8 @@
 
     MOCK_METHOD2(getAttachmentRenderTarget, gl::Error(const gl::FramebufferAttachment::Target &, FramebufferAttachmentRenderTarget **));
 
+    MOCK_METHOD1(setBaseLevel, void(GLuint));
+
     MOCK_METHOD0(destructor, void());
 };
 
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.cpp b/src/libANGLE/renderer/d3d/TextureD3D.cpp
index 11a08c0..afa4344 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.cpp
+++ b/src/libANGLE/renderer/d3d/TextureD3D.cpp
@@ -80,7 +80,8 @@
       mUsage(GL_NONE),
       mDirtyImages(true),
       mImmutable(false),
-      mTexStorage(NULL)
+      mTexStorage(nullptr),
+      mBaseLevel(0)
 {
 }
 
@@ -112,6 +113,21 @@
     return gl::Error(GL_NO_ERROR);
 }
 
+GLint TextureD3D::getLevelZeroWidth() const
+{
+    return getBaseLevelWidth() << mBaseLevel;
+}
+
+GLint TextureD3D::getLevelZeroHeight() const
+{
+    return getBaseLevelHeight() << mBaseLevel;
+}
+
+GLint TextureD3D::getLevelZeroDepth() const
+{
+    return getBaseLevelDepth();
+}
+
 GLint TextureD3D::getBaseLevelWidth() const
 {
     const ImageD3D *baseImage = getBaseLevelImage();
@@ -359,7 +375,9 @@
 
 int TextureD3D::mipLevels() const
 {
-    return gl::log2(std::max(std::max(getBaseLevelWidth(), getBaseLevelHeight()), getBaseLevelDepth())) + 1;
+    return gl::log2(
+               std::max(std::max(getLevelZeroWidth(), getLevelZeroHeight()), getLevelZeroDepth())) +
+           1;
 }
 
 TextureStorage *TextureD3D::getStorage()
@@ -370,7 +388,11 @@
 
 ImageD3D *TextureD3D::getBaseLevelImage() const
 {
-    return getImage(getImageIndex(0, 0));
+    if (mBaseLevel >= gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS)
+    {
+        return nullptr;
+    }
+    return getImage(getImageIndex(mBaseLevel, 0));
 }
 
 gl::Error TextureD3D::setImageExternal(GLenum target,
@@ -444,7 +466,7 @@
     GLint mipCount = mipLevels();
 
     // We know that all layers have the same dimension, for the texture to be complete
-    GLint layerCount = static_cast<GLint>(getLayerCount(0));
+    GLint layerCount = static_cast<GLint>(getLayerCount(getBaseLevel()));
 
     // When making mipmaps with the setData workaround enabled, the texture storage has
     // the image data already. For non-render-target storage, we have to pull it out into
@@ -540,7 +562,7 @@
         return true;
     }
 
-    if (baseImage->getTarget() == GL_TEXTURE_2D_ARRAY && getLayerCount(0) <= 0)
+    if (baseImage->getTarget() == GL_TEXTURE_2D_ARRAY && getLayerCount(getBaseLevel()) <= 0)
     {
         return true;
     }
@@ -621,6 +643,26 @@
     return error;
 }
 
+void TextureD3D::setBaseLevel(GLuint baseLevel)
+{
+    const int oldStorageWidth  = std::max(1, getLevelZeroWidth());
+    const int oldStorageHeight = std::max(1, getLevelZeroHeight());
+    const int oldStorageDepth  = std::max(1, getLevelZeroDepth());
+    mBaseLevel                 = baseLevel;
+
+    // When the base level changes, the texture storage might not be valid anymore, since it could
+    // have been created based on the dimensions of the previous specified level range.
+    const int newStorageWidth  = std::max(1, getLevelZeroWidth());
+    const int newStorageHeight = std::max(1, getLevelZeroHeight());
+    const int newStorageDepth = std::max(1, getLevelZeroDepth());
+    if (mTexStorage && (newStorageWidth != oldStorageWidth ||
+                        newStorageHeight != oldStorageHeight || newStorageDepth != oldStorageDepth))
+    {
+        markAllImagesDirty();
+        SafeDelete(mTexStorage);
+    }
+}
+
 TextureD3D_2D::TextureD3D_2D(RendererD3D *renderer)
     : TextureD3D(renderer)
 {
@@ -1025,9 +1067,8 @@
     int levelCount = mipLevels();
     for (int level = 1; level < levelCount; level++)
     {
-        gl::Extents levelSize(std::max(getBaseLevelWidth() >> level, 1),
-                              std::max(getBaseLevelHeight() >> level, 1),
-                              1);
+        gl::Extents levelSize(std::max(getLevelZeroWidth() >> level, 1),
+                              std::max(getLevelZeroHeight() >> level, 1), 1);
 
         redefineImage(level, getBaseLevelInternalFormat(), levelSize, false);
     }
@@ -1065,10 +1106,8 @@
         return true;
     }
 
-    const ImageD3D *baseImage = getBaseLevelImage();
-
-    GLsizei width = baseImage->getWidth();
-    GLsizei height = baseImage->getHeight();
+    GLsizei width  = getLevelZeroWidth();
+    GLsizei height = getLevelZeroHeight();
 
     if (width <= 0 || height <= 0)
     {
@@ -1076,15 +1115,15 @@
     }
 
     // The base image level is complete if the width and height are positive
-    if (level == 0)
+    if (level == static_cast<int>(getBaseLevel()))
     {
         return true;
     }
 
-    ASSERT(level >= 1 && level <= (int)ArraySize(mImageArray) && mImageArray[level] != NULL);
+    ASSERT(level >= 0 && level <= (int)ArraySize(mImageArray) && mImageArray[level] != nullptr);
     ImageD3D *image = mImageArray[level];
 
-    if (image->getInternalFormat() != baseImage->getInternalFormat())
+    if (image->getInternalFormat() != getBaseLevelInternalFormat())
     {
         return false;
     }
@@ -1117,7 +1156,7 @@
     }
 
     // do not attempt to create storage for nonexistant data
-    if (!isLevelComplete(0))
+    if (!isLevelComplete(getBaseLevel()))
     {
         return gl::Error(GL_NO_ERROR);
     }
@@ -1152,8 +1191,8 @@
 
 gl::Error TextureD3D_2D::createCompleteStorage(bool renderTarget, TextureStorage **outTexStorage) const
 {
-    GLsizei width = getBaseLevelWidth();
-    GLsizei height = getBaseLevelHeight();
+    GLsizei width         = getLevelZeroWidth();
+    GLsizei height        = getLevelZeroHeight();
     GLenum internalFormat = getBaseLevelInternalFormat();
 
     ASSERT(width > 0 && height > 0);
@@ -1247,8 +1286,8 @@
     ASSERT(size.depth == 1);
 
     // If there currently is a corresponding storage texture image, it has these parameters
-    const int storageWidth = std::max(1, getBaseLevelWidth() >> level);
-    const int storageHeight = std::max(1, getBaseLevelHeight() >> level);
+    const int storageWidth     = std::max(1, getLevelZeroWidth() >> level);
+    const int storageHeight    = std::max(1, getLevelZeroHeight() >> level);
     const GLenum storageFormat = getBaseLevelInternalFormat();
 
     mImageArray[level]->redefine(GL_TEXTURE_2D, internalformat, size, forceRelease);
@@ -1270,13 +1309,8 @@
             size.height != storageHeight ||
             internalformat != storageFormat)   // Discard mismatched storage
         {
-            for (size_t i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
-            {
-                mImageArray[i]->markDirty();
-            }
-
+            markAllImagesDirty();
             SafeDelete(mTexStorage);
-            mDirtyImages = true;
         }
     }
 
@@ -1301,6 +1335,15 @@
             index.mipIndex >= 0 && index.mipIndex < mTexStorage->getLevelCount());
 }
 
+void TextureD3D_2D::markAllImagesDirty()
+{
+    for (size_t i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
+    {
+        mImageArray[i]->markDirty();
+    }
+    mDirtyImages = true;
+}
+
 TextureD3D_Cube::TextureD3D_Cube(RendererD3D *renderer)
     : TextureD3D(renderer)
 {
@@ -1588,7 +1631,7 @@
 
     for (int faceIndex = 1; faceIndex < 6; faceIndex++)
     {
-        const ImageD3D &faceBaseImage = *mImageArray[faceIndex][0];
+        const ImageD3D &faceBaseImage = *mImageArray[faceIndex][getBaseLevel()];
 
         if (faceBaseImage.getWidth()          != baseWidth  ||
             faceBaseImage.getHeight()         != baseHeight ||
@@ -1656,7 +1699,7 @@
     }
 
     // do not attempt to create storage for nonexistant data
-    if (!isFaceLevelComplete(0, 0))
+    if (!isFaceLevelComplete(0, getBaseLevel()))
     {
         return gl::Error(GL_NO_ERROR);
     }
@@ -1691,7 +1734,7 @@
 
 gl::Error TextureD3D_Cube::createCompleteStorage(bool renderTarget, TextureStorage **outTexStorage) const
 {
-    GLsizei size = getBaseLevelWidth();
+    GLsizei size = getLevelZeroWidth();
 
     ASSERT(size > 0);
 
@@ -1772,16 +1815,21 @@
 
 bool TextureD3D_Cube::isFaceLevelComplete(int faceIndex, int level) const
 {
-    ASSERT(level >= 0 && faceIndex < 6 && level < (int)ArraySize(mImageArray[faceIndex]) && mImageArray[faceIndex][level] != NULL);
+    if (getBaseLevel() >= gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS)
+    {
+        return false;
+    }
+    ASSERT(level >= 0 && faceIndex < 6 && level < (int)ArraySize(mImageArray[faceIndex]) &&
+           mImageArray[faceIndex][level] != nullptr);
 
     if (isImmutable())
     {
         return true;
     }
 
-    int baseSize = getBaseLevelWidth();
+    int levelZeroSize = getLevelZeroWidth();
 
-    if (baseSize <= 0)
+    if (levelZeroSize <= 0)
     {
         return false;
     }
@@ -1802,7 +1850,7 @@
         return false;
     }
 
-    if (faceLevelImage->getWidth() != std::max(1, baseSize >> level))
+    if (faceLevelImage->getWidth() != std::max(1, levelZeroSize >> level))
     {
         return false;
     }
@@ -1838,8 +1886,8 @@
 void TextureD3D_Cube::redefineImage(int faceIndex, GLint level, GLenum internalformat, const gl::Extents &size)
 {
     // If there currently is a corresponding storage texture image, it has these parameters
-    const int storageWidth = std::max(1, getBaseLevelWidth() >> level);
-    const int storageHeight = std::max(1, getBaseLevelHeight() >> level);
+    const int storageWidth     = std::max(1, getLevelZeroWidth() >> level);
+    const int storageHeight    = std::max(1, getLevelZeroHeight() >> level);
     const GLenum storageFormat = getBaseLevelInternalFormat();
 
     mImageArray[faceIndex][level]->redefine(GL_TEXTURE_CUBE_MAP, internalformat, size, false);
@@ -1853,17 +1901,8 @@
             size.height != storageHeight ||
             internalformat != storageFormat)   // Discard mismatched storage
         {
-            for (int dirtyLevel = 0; dirtyLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; dirtyLevel++)
-            {
-                for (int dirtyFace = 0; dirtyFace < 6; dirtyFace++)
-                {
-                    mImageArray[dirtyFace][dirtyLevel]->markDirty();
-                }
-            }
-
+            markAllImagesDirty();
             SafeDelete(mTexStorage);
-
-            mDirtyImages = true;
         }
     }
 }
@@ -1885,6 +1924,18 @@
             index.mipIndex >= 0 && index.mipIndex < mTexStorage->getLevelCount());
 }
 
+void TextureD3D_Cube::markAllImagesDirty()
+{
+    for (int dirtyLevel = 0; dirtyLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; dirtyLevel++)
+    {
+        for (int dirtyFace = 0; dirtyFace < 6; dirtyFace++)
+        {
+            mImageArray[dirtyFace][dirtyLevel]->markDirty();
+        }
+    }
+    mDirtyImages = true;
+}
+
 TextureD3D_3D::TextureD3D_3D(RendererD3D *renderer)
     : TextureD3D(renderer)
 {
@@ -1991,7 +2042,7 @@
     gl::ImageIndex index = gl::ImageIndex::Make3D(level);
 
     // Attempt a fast gpu copy of the pixel data to the surface if the app bound an unpack buffer
-    if (isFastUnpackable(unpack, sizedInternalFormat) && !size.empty())
+    if (isFastUnpackable(unpack, sizedInternalFormat) && !size.empty() && isLevelComplete(level))
     {
         // Will try to create RT storage if it does not exist
         RenderTargetD3D *destRenderTarget = NULL;
@@ -2041,7 +2092,7 @@
     gl::ImageIndex index = gl::ImageIndex::Make3D(level);
 
     // Attempt a fast gpu copy of the pixel data to the surface if the app bound an unpack buffer
-    if (isFastUnpackable(unpack, getInternalFormat(level)))
+    if (isFastUnpackable(unpack, getInternalFormat(level)) && isLevelComplete(level))
     {
         RenderTargetD3D *destRenderTarget = NULL;
         gl::Error error = getRenderTarget(index, &destRenderTarget);
@@ -2209,9 +2260,9 @@
     int levelCount = mipLevels();
     for (int level = 1; level < levelCount; level++)
     {
-        gl::Extents levelSize(std::max(getBaseLevelWidth() >> level, 1),
-                              std::max(getBaseLevelHeight() >> level, 1),
-                              std::max(getBaseLevelDepth() >> level, 1));
+        gl::Extents levelSize(std::max(getLevelZeroWidth() >> level, 1),
+                              std::max(getLevelZeroHeight() >> level, 1),
+                              std::max(getLevelZeroDepth() >> level, 1));
         redefineImage(level, getBaseLevelInternalFormat(), levelSize);
     }
 }
@@ -2254,7 +2305,7 @@
     }
 
     // do not attempt to create storage for nonexistant data
-    if (!isLevelComplete(0))
+    if (!isLevelComplete(getBaseLevel()))
     {
         return gl::Error(GL_NO_ERROR);
     }
@@ -2289,9 +2340,9 @@
 
 gl::Error TextureD3D_3D::createCompleteStorage(bool renderTarget, TextureStorage **outStorage) const
 {
-    GLsizei width = getBaseLevelWidth();
-    GLsizei height = getBaseLevelHeight();
-    GLsizei depth = getBaseLevelDepth();
+    GLsizei width         = getLevelZeroWidth();
+    GLsizei height        = getLevelZeroHeight();
+    GLsizei depth         = getLevelZeroDepth();
     GLenum internalFormat = getBaseLevelInternalFormat();
 
     ASSERT(width > 0 && height > 0 && depth > 0);
@@ -2350,16 +2401,16 @@
         return true;
     }
 
-    GLsizei width = getBaseLevelWidth();
-    GLsizei height = getBaseLevelHeight();
-    GLsizei depth = getBaseLevelDepth();
+    GLsizei width  = getLevelZeroWidth();
+    GLsizei height = getLevelZeroHeight();
+    GLsizei depth  = getLevelZeroDepth();
 
     if (width <= 0 || height <= 0 || depth <= 0)
     {
         return false;
     }
 
-    if (level == 0)
+    if (level == static_cast<int>(getBaseLevel()))
     {
         return true;
     }
@@ -2416,15 +2467,15 @@
 void TextureD3D_3D::redefineImage(GLint level, GLenum internalformat, const gl::Extents &size)
 {
     // If there currently is a corresponding storage texture image, it has these parameters
-    const int storageWidth = std::max(1, getBaseLevelWidth() >> level);
-    const int storageHeight = std::max(1, getBaseLevelHeight() >> level);
-    const int storageDepth = std::max(1, getBaseLevelDepth() >> level);
-    const GLenum storageFormat = getBaseLevelInternalFormat();
+    const int storageWidth  = std::max(1, getLevelZeroWidth() >> level);
+    const int storageHeight = std::max(1, getLevelZeroHeight() >> level);
+    const int storageDepth  = std::max(1, getLevelZeroDepth() >> level);
 
     mImageArray[level]->redefine(GL_TEXTURE_3D, internalformat, size, false);
 
     if (mTexStorage)
     {
+        const GLenum storageFormat = getBaseLevelInternalFormat();
         const int storageLevels = mTexStorage->getLevelCount();
 
         if ((level >= storageLevels && storageLevels != 0) ||
@@ -2433,13 +2484,8 @@
             size.depth != storageDepth ||
             internalformat != storageFormat)   // Discard mismatched storage
         {
-            for (int i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
-            {
-                mImageArray[i]->markDirty();
-            }
-
+            markAllImagesDirty();
             SafeDelete(mTexStorage);
-            mDirtyImages = true;
         }
     }
 }
@@ -2462,6 +2508,20 @@
             index.mipIndex >= 0 && index.mipIndex < mTexStorage->getLevelCount());
 }
 
+void TextureD3D_3D::markAllImagesDirty()
+{
+    for (int i = 0; i < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; i++)
+    {
+        mImageArray[i]->markDirty();
+    }
+    mDirtyImages = true;
+}
+
+GLint TextureD3D_3D::getLevelZeroDepth() const
+{
+    return getBaseLevelDepth() << getBaseLevel();
+}
+
 TextureD3D_2DArray::TextureD3D_2DArray(RendererD3D *renderer)
     : TextureD3D(renderer)
 {
@@ -2708,8 +2768,10 @@
                 return error;
             }
 
-            error = mRenderer->copyImage2DArray(source, sourceArea, gl::GetInternalFormatInfo(getInternalFormat(0)).format,
-                                                destOffset, mTexStorage, level);
+            error = mRenderer->copyImage2DArray(
+                source, sourceArea,
+                gl::GetInternalFormatInfo(getInternalFormat(getBaseLevel())).format, destOffset,
+                mTexStorage, level);
             if (error.isError())
             {
                 return error;
@@ -2784,9 +2846,9 @@
 
 void TextureD3D_2DArray::initMipmapsImages()
 {
-    int baseWidth = getBaseLevelWidth();
-    int baseHeight = getBaseLevelHeight();
-    int baseDepth = getLayerCount(0);
+    int baseWidth     = getLevelZeroWidth();
+    int baseHeight    = getLevelZeroHeight();
+    int baseDepth     = getLayerCount(getBaseLevel());
     GLenum baseFormat = getBaseLevelInternalFormat();
 
     // Purge array levels 1 through q and reset them to represent the generated mipmap levels.
@@ -2827,7 +2889,7 @@
     }
 
     // do not attempt to create storage for nonexistant data
-    if (!isLevelComplete(0))
+    if (!isLevelComplete(getBaseLevel()))
     {
         return gl::Error(GL_NO_ERROR);
     }
@@ -2862,9 +2924,9 @@
 
 gl::Error TextureD3D_2DArray::createCompleteStorage(bool renderTarget, TextureStorage **outStorage) const
 {
-    GLsizei width = getBaseLevelWidth();
-    GLsizei height = getBaseLevelHeight();
-    GLsizei depth = getLayerCount(0);
+    GLsizei width         = getLevelZeroWidth();
+    GLsizei height        = getLevelZeroHeight();
+    GLsizei depth         = getLayerCount(getBaseLevel());
     GLenum internalFormat = getBaseLevelInternalFormat();
 
     ASSERT(width > 0 && height > 0 && depth > 0);
@@ -2923,21 +2985,29 @@
         return true;
     }
 
-    GLsizei width = getBaseLevelWidth();
-    GLsizei height = getBaseLevelHeight();
-    GLsizei layers = getLayerCount(0);
+    GLsizei width  = getLevelZeroWidth();
+    GLsizei height = getLevelZeroHeight();
 
-    if (width <= 0 || height <= 0 || layers <= 0)
+    if (width <= 0 || height <= 0)
     {
         return false;
     }
 
-    if (level == 0)
+    // Layers check needs to happen after the above checks, otherwise out-of-range base level may be
+    // queried.
+    GLsizei layers = getLayerCount(getBaseLevel());
+
+    if (layers <= 0)
+    {
+        return false;
+    }
+
+    if (level == static_cast<int>(getBaseLevel()))
     {
         return true;
     }
 
-    if (getInternalFormat(level) != getInternalFormat(0))
+    if (getInternalFormat(level) != getInternalFormat(getBaseLevel()))
     {
         return false;
     }
@@ -3005,10 +3075,14 @@
 void TextureD3D_2DArray::redefineImage(GLint level, GLenum internalformat, const gl::Extents &size)
 {
     // If there currently is a corresponding storage texture image, it has these parameters
-    const int storageWidth = std::max(1, getBaseLevelWidth() >> level);
-    const int storageHeight = std::max(1, getBaseLevelHeight() >> level);
-    const int storageDepth = getLayerCount(0);
-    const GLenum storageFormat = getBaseLevelInternalFormat();
+    const int storageWidth  = std::max(1, getLevelZeroWidth() >> level);
+    const int storageHeight = std::max(1, getLevelZeroHeight() >> level);
+    const GLuint baseLevel  = getBaseLevel();
+    int storageDepth = 0;
+    if (baseLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS)
+    {
+        storageDepth = getLayerCount(baseLevel);
+    }
 
     // Only reallocate the layers if the size doesn't match
     if (size.depth != mLayerCounts[level])
@@ -3041,6 +3115,7 @@
 
     if (mTexStorage)
     {
+        const GLenum storageFormat = getBaseLevelInternalFormat();
         const int storageLevels = mTexStorage->getLevelCount();
 
         if ((level >= storageLevels && storageLevels != 0) ||
@@ -3049,17 +3124,8 @@
             size.depth != storageDepth ||
             internalformat != storageFormat)   // Discard mismatched storage
         {
-            for (int dirtyLevel = 0; dirtyLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; dirtyLevel++)
-            {
-                for (int dirtyLayer = 0; dirtyLayer < mLayerCounts[dirtyLevel]; dirtyLayer++)
-                {
-                    mImageArray[dirtyLevel][dirtyLayer]->markDirty();
-                }
-            }
-
-            delete mTexStorage;
-            mTexStorage = NULL;
-            mDirtyImages = true;
+            markAllImagesDirty();
+            SafeDelete(mTexStorage);
         }
     }
 }
@@ -3092,6 +3158,18 @@
     return (!index.hasLayer() || (index.layerIndex >= 0 && index.layerIndex < mLayerCounts[index.mipIndex]));
 }
 
+void TextureD3D_2DArray::markAllImagesDirty()
+{
+    for (int dirtyLevel = 0; dirtyLevel < gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS; dirtyLevel++)
+    {
+        for (int dirtyLayer = 0; dirtyLayer < mLayerCounts[dirtyLevel]; dirtyLayer++)
+        {
+            mImageArray[dirtyLevel][dirtyLayer]->markDirty();
+        }
+    }
+    mDirtyImages = true;
+}
+
 TextureD3D_External::TextureD3D_External(RendererD3D *renderer) : TextureD3D(renderer)
 {
     mImage = renderer->createImage();
@@ -3338,4 +3416,9 @@
 {
     return (mTexStorage && index.type == GL_TEXTURE_EXTERNAL_OES && index.mipIndex == 0);
 }
+
+void TextureD3D_External::markAllImagesDirty()
+{
+    UNREACHABLE();
+}
 }
diff --git a/src/libANGLE/renderer/d3d/TextureD3D.h b/src/libANGLE/renderer/d3d/TextureD3D.h
index 8a8c16d..0ca802a 100644
--- a/src/libANGLE/renderer/d3d/TextureD3D.h
+++ b/src/libANGLE/renderer/d3d/TextureD3D.h
@@ -44,7 +44,6 @@
 
     GLint getBaseLevelWidth() const;
     GLint getBaseLevelHeight() const;
-    GLint getBaseLevelDepth() const;
     GLenum getBaseLevelInternalFormat() const;
 
     bool isImmutable() const { return mImmutable; }
@@ -69,6 +68,8 @@
     gl::Error getAttachmentRenderTarget(const gl::FramebufferAttachment::Target &target,
                                         FramebufferAttachmentRenderTarget **rtOut) override;
 
+    void setBaseLevel(GLuint baseLevel) override;
+
   protected:
     gl::Error setImageImpl(const gl::ImageIndex &index,
                            GLenum type,
@@ -87,6 +88,10 @@
     gl::Error fastUnpackPixels(const gl::PixelUnpackState &unpack, const uint8_t *pixels, const gl::Box &destArea,
                                GLenum sizedInternalFormat, GLenum type, RenderTargetD3D *destRenderTarget);
 
+    GLint getLevelZeroWidth() const;
+    GLint getLevelZeroHeight() const;
+    virtual GLint getLevelZeroDepth() const;
+
     GLint creationLevels(GLsizei width, GLsizei height, GLsizei depth) const;
     int mipLevels() const;
     virtual void initMipmapsImages() = 0;
@@ -100,6 +105,12 @@
     virtual gl::Error setCompleteTexStorage(TextureStorage *newCompleteTexStorage) = 0;
     gl::Error commitRegion(const gl::ImageIndex &index, const gl::Box &region);
 
+    GLuint getBaseLevel() const { return mBaseLevel; };
+
+    virtual void markAllImagesDirty() = 0;
+
+    GLint getBaseLevelDepth() const;
+
     RendererD3D *mRenderer;
 
     GLenum mUsage;
@@ -117,6 +128,8 @@
     bool shouldUseSetData(const ImageD3D *image) const;
 
     gl::Error generateMipmapsUsingImages();
+
+    GLuint mBaseLevel;
 };
 
 class TextureD3D_2D : public TextureD3D
@@ -162,6 +175,9 @@
     virtual gl::ImageIndex getImageIndex(GLint mip, GLint layer) const;
     virtual bool isValidIndex(const gl::ImageIndex &index) const;
 
+  protected:
+    void markAllImagesDirty() override;
+
   private:
     virtual gl::Error initializeStorage(bool renderTarget);
     virtual gl::Error createCompleteStorage(bool renderTarget, TextureStorage **outTexStorage) const;
@@ -230,6 +246,9 @@
     virtual gl::ImageIndex getImageIndex(GLint mip, GLint layer) const;
     virtual bool isValidIndex(const gl::ImageIndex &index) const;
 
+  protected:
+    void markAllImagesDirty() override;
+
   private:
     virtual gl::Error initializeStorage(bool renderTarget);
     virtual gl::Error createCompleteStorage(bool renderTarget, TextureStorage **outTexStorage) const;
@@ -293,6 +312,10 @@
     virtual gl::ImageIndex getImageIndex(GLint mip, GLint layer) const;
     virtual bool isValidIndex(const gl::ImageIndex &index) const;
 
+  protected:
+    void markAllImagesDirty() override;
+    GLint getLevelZeroDepth() const override;
+
   private:
     virtual gl::Error initializeStorage(bool renderTarget);
     virtual gl::Error createCompleteStorage(bool renderTarget, TextureStorage **outStorage) const;
@@ -354,6 +377,9 @@
     virtual gl::ImageIndex getImageIndex(GLint mip, GLint layer) const;
     virtual bool isValidIndex(const gl::ImageIndex &index) const;
 
+  protected:
+    void markAllImagesDirty() override;
+
   private:
     virtual gl::Error initializeStorage(bool renderTarget);
     virtual gl::Error createCompleteStorage(bool renderTarget, TextureStorage **outStorage) const;
@@ -454,6 +480,9 @@
     gl::ImageIndex getImageIndex(GLint mip, GLint layer) const override;
     bool isValidIndex(const gl::ImageIndex &index) const override;
 
+  protected:
+    void markAllImagesDirty() override;
+
   private:
     gl::Error initializeStorage(bool renderTarget) override;
     gl::Error createCompleteStorage(bool renderTarget,
diff --git a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
index 20f1afd..87877fd 100644
--- a/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
+++ b/src/libANGLE/renderer/d3d/d3d11/Renderer11.cpp
@@ -1298,7 +1298,7 @@
 
         // Make sure to add the level offset for our tiny compressed texture workaround
         gl::TextureState textureState = texture->getTextureState();
-        textureState.baseLevel += storage11->getTopLevel();
+        textureState.baseLevel        = texture->getEffectiveBaseLevel() + storage11->getTopLevel();
 
         error = storage11->getSRV(textureState, &textureSRV);
         if (error.isError())
@@ -2413,8 +2413,8 @@
 
 void Renderer11::SamplerMetadataD3D11::update(unsigned int samplerIndex, const gl::Texture &texture)
 {
-    unsigned int baseLevel = texture.getBaseLevel();
-    GLenum internalFormat = texture.getInternalFormat(texture.getTarget(), texture.getBaseLevel());
+    unsigned int baseLevel = texture.getEffectiveBaseLevel();
+    GLenum internalFormat = texture.getInternalFormat(texture.getTarget(), baseLevel);
     if (mSamplerMetadata[samplerIndex].baseLevel != static_cast<int>(baseLevel))
     {
         mSamplerMetadata[samplerIndex].baseLevel = static_cast<int>(baseLevel);
diff --git a/src/libANGLE/renderer/gl/StateManagerGL.cpp b/src/libANGLE/renderer/gl/StateManagerGL.cpp
index 7b84e55..aa189f4 100644
--- a/src/libANGLE/renderer/gl/StateManagerGL.cpp
+++ b/src/libANGLE/renderer/gl/StateManagerGL.cpp
@@ -729,7 +729,8 @@
                     bindTexture(textureType, textureGL->getTextureID());
                 }
 
-                textureGL->syncState(textureUnitIndex, texture->getTextureState());
+                textureGL->syncState(textureUnitIndex, texture->getTextureState(),
+                                     texture->getEffectiveBaseLevel());
             }
             else
             {
diff --git a/src/libANGLE/renderer/gl/TextureGL.cpp b/src/libANGLE/renderer/gl/TextureGL.cpp
index 87a307d..d089ec6 100644
--- a/src/libANGLE/renderer/gl/TextureGL.cpp
+++ b/src/libANGLE/renderer/gl/TextureGL.cpp
@@ -117,7 +117,7 @@
       mWorkarounds(workarounds),
       mStateManager(stateManager),
       mBlitter(blitter),
-      mLevelInfo(gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS),
+      mLevelInfo(gl::IMPLEMENTATION_MAX_TEXTURE_LEVELS + 1),
       mAppliedTextureState(),
       mTextureID(0)
 {
@@ -712,7 +712,9 @@
     }
 }
 
-void TextureGL::syncState(size_t textureUnit, const gl::TextureState &textureState) const
+void TextureGL::syncState(size_t textureUnit,
+                          const gl::TextureState &textureState,
+                          const GLuint effectiveBaseLevel) const
 {
     // Callback lamdba to bind this texture only if needed.
     bool textureApplied   = false;
@@ -732,7 +734,7 @@
     SyncTextureStateMember(mFunctions, applyTextureFunc, textureState, mAppliedTextureState, mTextureType, GL_TEXTURE_BASE_LEVEL, &gl::TextureState::baseLevel);
     SyncTextureStateMember(mFunctions, applyTextureFunc, textureState, mAppliedTextureState, mTextureType, GL_TEXTURE_MAX_LEVEL, &gl::TextureState::maxLevel);
 
-    const LevelInfoGL &levelInfo = mLevelInfo[textureState.baseLevel];
+    const LevelInfoGL &levelInfo = mLevelInfo[effectiveBaseLevel];
     SyncTextureStateSwizzle(mFunctions, applyTextureFunc, levelInfo, textureState, mAppliedTextureState, mTextureType, GL_TEXTURE_SWIZZLE_R, &gl::TextureState::swizzleRed);
     SyncTextureStateSwizzle(mFunctions, applyTextureFunc, levelInfo, textureState, mAppliedTextureState, mTextureType, GL_TEXTURE_SWIZZLE_G, &gl::TextureState::swizzleGreen);
     SyncTextureStateSwizzle(mFunctions, applyTextureFunc, levelInfo, textureState, mAppliedTextureState, mTextureType, GL_TEXTURE_SWIZZLE_B, &gl::TextureState::swizzleBlue);
diff --git a/src/libANGLE/renderer/gl/TextureGL.h b/src/libANGLE/renderer/gl/TextureGL.h
index 4449d01..e76bb66 100644
--- a/src/libANGLE/renderer/gl/TextureGL.h
+++ b/src/libANGLE/renderer/gl/TextureGL.h
@@ -88,7 +88,9 @@
 
     gl::Error setEGLImageTarget(GLenum target, egl::Image *image) override;
 
-    void syncState(size_t textureUnit, const gl::TextureState &textureState) const;
+    void syncState(size_t textureUnit,
+                   const gl::TextureState &textureState,
+                   const GLuint effectiveBaseLevel) const;
     GLuint getTextureID() const;
 
     gl::Error getAttachmentRenderTarget(const gl::FramebufferAttachment::Target &target,
@@ -97,6 +99,8 @@
         return gl::Error(GL_OUT_OF_MEMORY, "Not supported on OpenGL");
     }
 
+    void setBaseLevel(GLuint) override {}
+
   private:
     GLenum mTextureType;
 
diff --git a/src/libANGLE/validationES.cpp b/src/libANGLE/validationES.cpp
index 0b589ff..a84d9eb 100644
--- a/src/libANGLE/validationES.cpp
+++ b/src/libANGLE/validationES.cpp
@@ -1008,16 +1008,24 @@
 
       case GL_TEXTURE_BASE_LEVEL:
       case GL_TEXTURE_MAX_LEVEL:
-        if (param < 0)
-        {
-            context->handleError(Error(GL_INVALID_VALUE));
-            return false;
-        }
-        return true;
-
+          if (target == GL_TEXTURE_EXTERNAL_OES)
+          {
+              // This is not specified, but in line with the spirit of OES_EGL_image_external spec,
+              // which generally forbids setting mipmap related parameters on external textures.
+              context->handleError(
+                  Error(GL_INVALID_OPERATION,
+                        "Setting the base level or max level of external textures not supported"));
+              return false;
+          }
+          if (param < 0)
+          {
+              context->handleError(Error(GL_INVALID_VALUE));
+              return false;
+          }
+          return true;
       default:
           context->handleError(Error(GL_INVALID_ENUM));
-        return false;
+          return false;
     }
 }
 
diff --git a/src/tests/gl_tests/TextureTest.cpp b/src/tests/gl_tests/TextureTest.cpp
index 22dfac3..40f2162 100644
--- a/src/tests/gl_tests/TextureTest.cpp
+++ b/src/tests/gl_tests/TextureTest.cpp
@@ -268,7 +268,7 @@
 
         int testImageChannels = std::min(sourceImageChannels, destImageChannels);
 
-        EXPECT_PIXEL_EQ(0, 0, 255, 0, 0, 255);
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::red);
         if (testImageChannels > 1)
         {
             EXPECT_PIXEL_EQ(getWindowHeight() - 1, 0, 0, 255, 0, 255);
@@ -780,6 +780,60 @@
     GLint mTexture1Location;
 };
 
+class Texture3DTestES3 : public TexCoordDrawTest
+{
+  protected:
+    Texture3DTestES3() : TexCoordDrawTest(), mTexture3D(0), mTexture3DUniformLocation(-1) {}
+
+    std::string getVertexShaderSource() override
+    {
+        return std::string(
+            "#version 300 es\n"
+            "out vec2 texcoord;\n"
+            "in vec4 position;\n"
+            "void main()\n"
+            "{\n"
+            "    gl_Position = vec4(position.xy, 0.0, 1.0);\n"
+            "    texcoord = (position.xy * 0.5) + 0.5;\n"
+            "}\n");
+    }
+
+    std::string getFragmentShaderSource() override
+    {
+        return std::string(
+            "#version 300 es\n"
+            "precision highp float;\n"
+            "uniform highp sampler3D tex3D;\n"
+            "in vec2 texcoord;\n"
+            "out vec4 fragColor;\n"
+            "void main()\n"
+            "{\n"
+            "    fragColor = texture(tex3D, vec3(texcoord, 0.0));\n"
+            "}\n");
+    }
+
+    void SetUp() override
+    {
+        TexCoordDrawTest::SetUp();
+
+        glGenTextures(1, &mTexture3D);
+
+        setUpProgram();
+
+        mTexture3DUniformLocation = glGetUniformLocation(mProgram, "tex3D");
+        ASSERT_NE(-1, mTexture3DUniformLocation);
+    }
+
+    void TearDown() override
+    {
+        glDeleteTextures(1, &mTexture3D);
+        TexCoordDrawTest::TearDown();
+    }
+
+    GLuint mTexture3D;
+    GLint mTexture3DUniformLocation;
+};
+
 class ShadowSamplerPlusSampler3DTestES3 : public TexCoordDrawTest
 {
   protected:
@@ -990,11 +1044,10 @@
 
         glActiveTexture(GL_TEXTURE0);
         glBindTexture(GL_TEXTURE_2D, mTexture2D);
-        GLubyte texDataGreen[1u * 1u * 4u];
-        FillWithRGBA<GLubyte>(1u * 1u, 0u, 255u, 0u, 255u, texDataGreen);
-        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataGreen);
+        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                     &GLColor::green);
         drawQuad(mProgram, "position", 0.5f);
-        EXPECT_PIXEL_EQ(0, 0, 0, 255, 0, 255);
+        EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
     }
 };
 
@@ -1232,11 +1285,9 @@
     glActiveTexture(GL_TEXTURE0);
     glBindTexture(GL_TEXTURE_2D, mTexture2D);
 
-    // Fill with red
-    std::vector<GLubyte> pixels(4 * 16 * 16);
-    FillWithRGBA<GLubyte>(16u * 16u, 255u, 0u, 0u, 255u, pixels.data());
+    std::vector<GLColor> pixelsRed(16u * 16u, GLColor::red);
 
-    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixelsRed.data());
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glGenerateMipmap(GL_TEXTURE_2D);
@@ -1246,24 +1297,24 @@
     glUniform2f(mDrawScaleUniformLocation, 0.0625f, 0.0625f);
     drawQuad(mProgram, "position", 0.5f);
     EXPECT_GL_NO_ERROR();
-    EXPECT_PIXEL_EQ(px, py, 255, 0, 0, 255);
+    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::red);
 
-    // Fill with blue
-    FillWithRGBA<GLubyte>(16u * 16u, 0u, 0u, 255u, 255u, pixels.data());
+    std::vector<GLColor> pixelsBlue(16u * 16u, GLColor::blue);
 
-    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 pixelsBlue.data());
     glGenerateMipmap(GL_TEXTURE_2D);
 
-    // Fill with green
-    FillWithRGBA<GLubyte>(16u * 16u, 0u, 255u, 0u, 255u, pixels.data());
+    std::vector<GLColor> pixelsGreen(16u * 16u, GLColor::green);
 
-    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels.data());
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 16, 16, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 pixelsGreen.data());
     glGenerateMipmap(GL_TEXTURE_2D);
 
     drawQuad(mProgram, "position", 0.5f);
 
     EXPECT_GL_NO_ERROR();
-    EXPECT_PIXEL_EQ(px, py, 0, 255, 0, 255);
+    EXPECT_PIXEL_COLOR_EQ(px, py, GLColor::green);
 }
 
 // Test creating a FBO with a cube map render target, to test an ANGLE bug
@@ -1598,13 +1649,14 @@
 {
     glActiveTexture(GL_TEXTURE0);
     glBindTexture(GL_TEXTURE_2D, mTexture2D);
-    GLubyte texDataRed[4u * 4u * 4u];
-    FillWithRGBA<GLubyte>(4u * 4u, 255u, 0u, 0u, 255u, texDataRed);
-    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataRed);
-    GLubyte texDataGreen[2u * 2u * 4u];
-    FillWithRGBA<GLubyte>(2u * 2u, 0u, 255u, 0u, 255u, texDataGreen);
-    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataGreen);
-    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataGreen);
+
+    std::vector<GLColor> texDataRed(4u * 4u, GLColor::red);
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, 4, 4, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataRed.data());
+    std::vector<GLColor> texDataGreen(2u * 2u, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
     glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
@@ -1613,7 +1665,527 @@
 
     drawQuad(mProgram, "position", 0.5f);
 
-    EXPECT_PIXEL_EQ(0, 0, 0, 255, 0, 255);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range do not
+// have images defined.
+TEST_P(Texture2DTestES3, DrawWithLevelsOutsideRangeUndefined)
+{
+    if (IsAMD() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Observed crashing on AMD. Oddly the crash only happens with 2D textures, not 3D or array.
+        std::cout << "Test skipped on AMD OpenGL." << std::endl;
+        return;
+    }
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+    std::vector<GLColor> texDataGreen(2u * 2u, GLColor::green);
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range have
+// dimensions that don't fit the images inside the range.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture2DTestES3, DrawWithLevelsOutsideRangeWithInconsistentDimensions)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+    std::vector<GLColor> texDataRed(8u * 8u, GLColor::red);
+    std::vector<GLColor> texDataGreen(2u * 2u, GLColor::green);
+    std::vector<GLColor> texDataCyan(2u * 2u, GLColor::cyan);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // Two levels that are initially unused.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE, texDataRed.data());
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataCyan.data());
+
+    // One level that is used - only this level should affect completeness.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed drawing color 0,0,0,0 instead of the texture color after the base
+        // level was changed.
+        std::cout << "Test partially skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+
+    // Switch the level that is being used to the cyan level 2.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range do not
+// have images defined.
+TEST_P(Texture3DTestES3, DrawWithLevelsOutsideRangeUndefined)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_3D, mTexture3D);
+    std::vector<GLColor> texDataGreen(2u * 2u * 2u, GLColor::green);
+    glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range have
+// dimensions that don't fit the images inside the range.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture3DTestES3, DrawWithLevelsOutsideRangeWithInconsistentDimensions)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_3D, mTexture3D);
+    std::vector<GLColor> texDataRed(8u * 8u * 8u, GLColor::red);
+    std::vector<GLColor> texDataGreen(2u * 2u * 2u, GLColor::green);
+    std::vector<GLColor> texDataCyan(2u * 2u * 2u, GLColor::cyan);
+
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // Two levels that are initially unused.
+    glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataRed.data());
+    glTexImage3D(GL_TEXTURE_3D, 2, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataCyan.data());
+
+    // One level that is used - only this level should affect completeness.
+    glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed drawing color 0,0,0,0 instead of the texture color after the base
+        // level was changed.
+        std::cout << "Test partially skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+
+    // Switch the level that is being used to the cyan level 2.
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 2);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range do not
+// have images defined.
+TEST_P(Texture2DArrayTestES3, DrawWithLevelsOutsideRangeUndefined)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D_ARRAY, m2DArrayTexture);
+    std::vector<GLColor> texDataGreen(2u * 2u * 2u, GLColor::green);
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
+// Test that drawing works correctly when levels outside the BASE_LEVEL/MAX_LEVEL range have
+// dimensions that don't fit the images inside the range.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture2DArrayTestES3, DrawWithLevelsOutsideRangeWithInconsistentDimensions)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_3D, m2DArrayTexture);
+    std::vector<GLColor> texDataRed(8u * 8u * 8u, GLColor::red);
+    std::vector<GLColor> texDataGreen(2u * 2u * 2u, GLColor::green);
+    std::vector<GLColor> texDataCyan(2u * 2u * 2u, GLColor::cyan);
+
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // Two levels that are initially unused.
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGBA8, 8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataRed.data());
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 2, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataCyan.data());
+
+    // One level that is used - only this level should affect completeness.
+    glTexImage3D(GL_TEXTURE_2D_ARRAY, 1, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 1);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed drawing color 0,0,0,0 instead of the texture color after the base
+        // level was changed.
+        std::cout << "Test partially skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+    if (IsNVIDIA() && (getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE ||
+                       getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGLES_ANGLE))
+    {
+        // NVIDIA was observed drawing color 0,0,0,0 instead of the texture color after the base
+        // level was changed.
+        std::cout << "Test partially skipped on NVIDIA OpenGL." << std::endl;
+        return;
+    }
+
+    // Switch the level that is being used to the cyan level 2.
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAX_LEVEL, 2);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::cyan);
+}
+
+// Test that texture completeness is updated if texture max level changes.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture2DTestES3, TextureCompletenessChangesWithMaxLevel)
+{
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed having wrong behavior after the texture is made incomplete by changing
+        // the base level.
+        std::cout << "Test skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+    std::vector<GLColor> texDataGreen(8u * 8u, GLColor::green);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // A level that is initially unused.
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    // One level that is initially used - only this level should affect completeness.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    // Switch the max level to level 1. The levels within the used range now have inconsistent
+    // dimensions and the texture should be incomplete.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
+// Test that 3D texture completeness is updated if texture max level changes.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture3DTestES3, Texture3DCompletenessChangesWithMaxLevel)
+{
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed having wrong behavior after the texture is made incomplete by changing
+        // the base level.
+        std::cout << "Test skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_3D, mTexture3D);
+    std::vector<GLColor> texDataGreen(2u * 2u * 2u, GLColor::green);
+
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // A level that is initially unused.
+    glTexImage3D(GL_TEXTURE_3D, 1, GL_RGBA8, 1, 1, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    // One level that is initially used - only this level should affect completeness.
+    glTexImage3D(GL_TEXTURE_3D, 0, GL_RGBA8, 2, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_BASE_LEVEL, 0);
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 0);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    // Switch the max level to level 1. The levels within the used range now have inconsistent
+    // dimensions and the texture should be incomplete.
+    glTexParameteri(GL_TEXTURE_3D, GL_TEXTURE_MAX_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
+// Test that texture completeness is updated if texture base level changes.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture2DTestES3, TextureCompletenessChangesWithBaseLevel)
+{
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel was observed having wrong behavior after the texture is made incomplete by changing
+        // the base level.
+        std::cout << "Test skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+    std::vector<GLColor> texDataGreen(8u * 8u, GLColor::green);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
+
+    // Two levels that are initially unused.
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 8, 8, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+    glTexImage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    // One level that is initially used - only this level should affect completeness.
+    glTexImage2D(GL_TEXTURE_2D, 2, GL_RGBA8, 2, 2, 0, GL_RGBA, GL_UNSIGNED_BYTE,
+                 texDataGreen.data());
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 2);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 2);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+
+    // Switch the base level to level 1. The levels within the used range now have inconsistent
+    // dimensions and the texture should be incomplete.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 1);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
+// Test that texture is not complete if base level is greater than max level.
+// GLES 3.0.4 section 3.8.13 Texture completeness
+TEST_P(Texture2DTestES3, TextureBaseLevelGreaterThanMaxLevel)
+{
+    if (IsIntel() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Intel Windows OpenGL driver crashes if the base level of a non-immutable texture is out
+        // of range.
+        std::cout << "Test skipped on Intel OpenGL." << std::endl;
+        return;
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::green);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10000);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 0);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    // Texture should be incomplete.
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::black);
+}
+
+// Test that immutable texture base level and max level are clamped.
+// GLES 3.0.4 section 3.8.10 subsection Mipmapping
+TEST_P(Texture2DTestES3, ImmutableTextureBaseLevelOutOfRange)
+{
+    if (IsOSX())
+    {
+        // Observed incorrect rendering on OSX.
+        std::cout << "Test skipped on OSX." << std::endl;
+        return;
+    }
+    if (IsAMD() && getPlatformRenderer() == EGL_PLATFORM_ANGLE_TYPE_OPENGL_ANGLE)
+    {
+        // Observed incorrect rendering on AMD OpenGL.
+        std::cout << "Test skipped on AMD OpenGL." << std::endl;
+        return;
+    }
+
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    glTexStorage2D(GL_TEXTURE_2D, 1, GL_RGBA8, 1, 1);
+
+    glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::green);
+
+    // For immutable-format textures, base level should be clamped to [0, levels - 1], and max level
+    // should be clamped to [base_level, levels - 1].
+    // GLES 3.0.4 section 3.8.10 subsection Mipmapping
+    // In the case of this test, those rules make the effective base level and max level 0.
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10000);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 10000);
+
+    EXPECT_GL_NO_ERROR();
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    // Texture should be complete.
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
+}
+
+// Test that setting a texture image works when base level is out of range.
+TEST_P(Texture2DTestES3, SetImageWhenBaseLevelOutOfRange)
+{
+    glActiveTexture(GL_TEXTURE0);
+    glBindTexture(GL_TEXTURE_2D, mTexture2D);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 10000);
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 10000);
+
+    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, 1, 1, 0, GL_RGBA, GL_UNSIGNED_BYTE, &GLColor::green);
+
+    EXPECT_GL_NO_ERROR();
+
+    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_BASE_LEVEL, 0);
+
+    drawQuad(mProgram, "position", 0.5f);
+
+    // Texture should be complete.
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
 }
 
 // In the D3D11 renderer, we need to initialize some texture formats, to fill empty channels. EG RBA->RGBA8, with 1.0
@@ -1641,12 +2213,12 @@
     glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_REPEAT);
     glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_REPEAT);
     drawQuad(mProgram, "position", 1.0f);
-    EXPECT_PIXEL_EQ(0, 0, 0, 255, 0, 255);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
 
     // The dimension of the respecification must match the original exactly to trigger the bug.
     glTexImage3D(GL_TEXTURE_2D_ARRAY, 0, GL_RGB, 4, 4, 2, 0, GL_RGB, GL_UNSIGNED_BYTE, &pixelData[0]);
     drawQuad(mProgram, "position", 1.0f);
-    EXPECT_PIXEL_EQ(0, 0, 0, 255, 0, 255);
+    EXPECT_PIXEL_COLOR_EQ(0, 0, GLColor::green);
 
     ASSERT_GL_NO_ERROR();
 }
@@ -2538,6 +3110,7 @@
                        ES2_OPENGL(),
                        ES2_OPENGLES());
 ANGLE_INSTANTIATE_TEST(Texture2DTestES3, ES3_D3D11(), ES3_OPENGL(), ES3_OPENGLES());
+ANGLE_INSTANTIATE_TEST(Texture3DTestES3, ES3_D3D11(), ES3_OPENGL(), ES3_OPENGLES());
 ANGLE_INSTANTIATE_TEST(Texture2DIntegerAlpha1TestES3, ES3_D3D11(), ES3_OPENGL(), ES3_OPENGLES());
 ANGLE_INSTANTIATE_TEST(Texture2DUnsignedIntegerAlpha1TestES3,
                        ES3_D3D11(),
diff --git a/src/tests/test_utils/ANGLETest.cpp b/src/tests/test_utils/ANGLETest.cpp
index 15135a5..4115b4d 100644
--- a/src/tests/test_utils/ANGLETest.cpp
+++ b/src/tests/test_utils/ANGLETest.cpp
@@ -15,6 +15,12 @@
 namespace angle
 {
 
+const GLColor GLColor::red   = GLColor(255u, 0u, 0u, 255u);
+const GLColor GLColor::green = GLColor(0u, 255u, 0u, 255u);
+const GLColor GLColor::blue  = GLColor(0u, 0u, 255u, 255u);
+const GLColor GLColor::cyan  = GLColor(0u, 255u, 255u, 255u);
+const GLColor GLColor::black = GLColor(0u, 0u, 0u, 255u);
+
 namespace
 {
 float ColorNorm(GLubyte channelValue)
diff --git a/src/tests/test_utils/ANGLETest.h b/src/tests/test_utils/ANGLETest.h
index 17badb4..744d365 100644
--- a/src/tests/test_utils/ANGLETest.h
+++ b/src/tests/test_utils/ANGLETest.h
@@ -52,6 +52,12 @@
     Vector4 toNormalizedVector() const;
 
     GLubyte R, G, B, A;
+
+    static const GLColor red;
+    static const GLColor green;
+    static const GLColor blue;
+    static const GLColor cyan;
+    static const GLColor black;
 };
 
 // Useful to cast any type to GLubyte.