/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "WebGLTexture.h"

#include <algorithm>
#include "CanvasUtils.h"
#include "GLBlitHelper.h"
#include "GLContext.h"
#include "mozilla/dom/HTMLVideoElement.h"
#include "mozilla/dom/ImageData.h"
#include "mozilla/gfx/SourceSurfaceRawData.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Scoped.h"
#include "mozilla/unused.h"
#include "ScopedGLHelpers.h"
#include "TexUnpackBlob.h"
#include "WebGLContext.h"
#include "WebGLContextUtils.h"
#include "WebGLFramebuffer.h"
#include "WebGLTexelConversions.h"

namespace mozilla {

/* This file handles:
 * TexStorage2D(texTarget, levels, internalFormat, width, height)
 * TexStorage3D(texTarget, levels, intenralFormat, width, height, depth)
 *
 * TexImage2D(texImageTarget, level, internalFormat, width, height, border, unpackFormat,
 *            unpackType, data)
 * TexImage3D(texImageTarget, level, internalFormat, width, height, depth, border,
 *            unpackFormat, unpackType, data)
 * TexSubImage2D(texImageTarget, level, xOffset, yOffset, width, height, unpackFormat,
 *               unpackType, data)
 * TexSubImage3D(texImageTarget, level, xOffset, yOffset, zOffset, width, height, depth,
 *               unpackFormat, unpackType, data)
 *
 * CompressedTexImage2D(texImageTarget, level, internalFormat, width, height, border,
 *                      imageSize, data)
 * CompressedTexImage3D(texImageTarget, level, internalFormat, width, height, depth,
 *                      border, imageSize, data)
 * CompressedTexSubImage2D(texImageTarget, level, xOffset, yOffset, width, height,
 *                         sizedUnpackFormat, imageSize, data)
 * CompressedTexSubImage3D(texImageTarget, level, xOffset, yOffset, zOffset, width,
 *                         height, depth, sizedUnpackFormat, imageSize, data)
 *
 * CopyTexImage2D(texImageTarget, level, internalFormat, x, y, width, height, border)
 * CopyTexImage3D - "Because the framebuffer is inhererntly two-dimensional, there is no
 *                   CopyTexImage3D command."
 * CopyTexSubImage2D(texImageTarget, level, xOffset, yOffset, x, y, width, height)
 * CopyTexSubImage3D(texImageTarget, level, xOffset, yOffset, zOffset, x, y, width,
 *                   height)
 */

////////////////////////////////////////
// ArrayBufferView?

static inline bool
DoesJSTypeMatchUnpackType(GLenum unpackType, js::Scalar::Type jsType)
{
    switch (unpackType) {
    case LOCAL_GL_BYTE:
        return jsType == js::Scalar::Type::Int8;

    case LOCAL_GL_UNSIGNED_BYTE:
        return jsType == js::Scalar::Type::Uint8 ||
               jsType == js::Scalar::Type::Uint8Clamped;

    case LOCAL_GL_SHORT:
        return jsType == js::Scalar::Type::Int16;

    case LOCAL_GL_UNSIGNED_SHORT:
    case LOCAL_GL_UNSIGNED_SHORT_4_4_4_4:
    case LOCAL_GL_UNSIGNED_SHORT_5_5_5_1:
    case LOCAL_GL_UNSIGNED_SHORT_5_6_5:
    case LOCAL_GL_HALF_FLOAT:
    case LOCAL_GL_HALF_FLOAT_OES:
        return jsType == js::Scalar::Type::Uint16;

    case LOCAL_GL_INT:
        return jsType == js::Scalar::Type::Int32;

    case LOCAL_GL_UNSIGNED_INT:
    case LOCAL_GL_UNSIGNED_INT_2_10_10_10_REV:
    case LOCAL_GL_UNSIGNED_INT_10F_11F_11F_REV:
    case LOCAL_GL_UNSIGNED_INT_5_9_9_9_REV:
    case LOCAL_GL_UNSIGNED_INT_24_8:
        return jsType == js::Scalar::Type::Uint32;

    case LOCAL_GL_FLOAT:
        return jsType == js::Scalar::Type::Float32;

    default:
        return false;
    }
}

static bool
ValidateUnpackArrayType(WebGLContext* webgl, const char* funcName, GLenum unpackType,
                        js::Scalar::Type jsType)
{
    if (DoesJSTypeMatchUnpackType(unpackType, jsType))
        return true;

    const auto& fua = webgl->mFormatUsage;
    const GLenum fakeUnpackFormat = LOCAL_GL_RGBA;
    if (!fua->AreUnpackEnumsValid(fakeUnpackFormat, unpackType)) {
        webgl->ErrorInvalidEnum("%s: Invalid unpack type: 0x%04x", funcName, unpackType);
        return false;
    }

    webgl->ErrorInvalidOperation("%s: `pixels` be compatible with unpack `type`.",
                                 funcName);
    return false;
}

static UniquePtr<webgl::TexUnpackBlob>
UnpackBlobFromMaybeView(WebGLContext* webgl, const char* funcName, GLsizei width,
                        GLsizei height, GLsizei depth, GLenum unpackType,
                        const dom::Nullable<dom::ArrayBufferView>& maybeView)
{
    size_t dataSize;
    const void* data;
    if (maybeView.IsNull()) {
        dataSize = 0;
        data = nullptr;
    } else {
        const auto& view = maybeView.Value();
        view.ComputeLengthAndData();
        data = view.DataAllowShared();
        dataSize = view.LengthAllowShared();
        js::Scalar::Type jsType = JS_GetArrayBufferViewType(view.Obj());
        if (!ValidateUnpackArrayType(webgl, funcName, unpackType, jsType))
            return nullptr;
    }

    UniquePtr<webgl::TexUnpackBlob> ret;
    // Warning: Possibly shared memory.  See bug 1225033.
    ret.reset(new webgl::TexUnpackBytes(width, height, depth, dataSize, data));
    return Move(ret);
}

void
WebGLTexture::TexOrSubImage(bool isSubImage, const char* funcName, TexImageTarget target,
                            GLint level, GLenum internalFormat, GLint xOffset,
                            GLint yOffset, GLint zOffset, GLsizei width, GLsizei height,
                            GLsizei depth, GLint border, GLenum unpackFormat,
                            GLenum unpackType,
                            const dom::Nullable<dom::ArrayBufferView>& maybeView)
{
    UniquePtr<webgl::TexUnpackBlob> blob;
    blob = UnpackBlobFromMaybeView(mContext, funcName, width, height, depth, unpackType,
                                   maybeView);
    if (!blob)
        return;

    TexOrSubImage(isSubImage, funcName, target, level, internalFormat, xOffset, yOffset,
                  zOffset, border, unpackFormat, unpackType, blob.get());
}

////////////////////////////////////////
// ImageData

static UniquePtr<webgl::TexUnpackBlob>
UnpackBlobFromImageData(WebGLContext* webgl, const char* funcName, GLenum unpackType,
                        dom::ImageData* imageData, dom::Uint8ClampedArray* scopedArr)
{
    if (!imageData) {
        // Spec says to generate an INVALID_VALUE error
        webgl->ErrorInvalidValue("%s: null ImageData", funcName);
        return nullptr;
    }

    DebugOnly<bool> inited = scopedArr->Init(imageData->GetDataObject());
    MOZ_ASSERT(inited);

    scopedArr->ComputeLengthAndData();
    const DebugOnly<size_t> dataSize = scopedArr->Length();
    const void* const data = scopedArr->Data();

    const gfx::IntSize size(imageData->Width(), imageData->Height());
    const size_t stride = size.width * 4;
    const gfx::SurfaceFormat surfFormat = gfx::SurfaceFormat::R8G8B8A8;
    const bool ownsData = false;

    MOZ_ASSERT(dataSize == stride * size.height);

    const RefPtr<gfx::SourceSurfaceRawData> surf = new gfx::SourceSurfaceRawData;

    uint8_t* wrappableData = (uint8_t*)data;
    surf->InitWrappingData(wrappableData, size, stride, surfFormat, ownsData);

    // WhatWG "HTML Living Standard" (30 October 2015):
    // "The getImageData(sx, sy, sw, sh) method [...] Pixels must be returned as
    //  non-premultiplied alpha values."
    const bool surfIsAlphaPremult = false;

    UniquePtr<webgl::TexUnpackBlob> ret;
    ret.reset(new webgl::TexUnpackSurface(surf, surfIsAlphaPremult));
    return Move(ret);
}

void
WebGLTexture::TexOrSubImage(bool isSubImage, const char* funcName, TexImageTarget target,
                            GLint level, GLenum internalFormat, GLint xOffset,
                            GLint yOffset, GLint zOffset, GLenum unpackFormat,
                            GLenum unpackType, dom::ImageData* imageData)
{
    dom::Uint8ClampedArray scopedArr;

    UniquePtr<webgl::TexUnpackBlob> blob;
    blob = UnpackBlobFromImageData(mContext, funcName, unpackType, imageData, &scopedArr);
    if (!blob)
        return;

    const GLint border = 0;
    TexOrSubImage(isSubImage, funcName, target, level, internalFormat, xOffset, yOffset,
                  zOffset, border, unpackFormat, unpackType, blob.get());
}

////////////////////////////////////////
// dom::Element

void
WebGLTexture::TexOrSubImage(bool isSubImage, const char* funcName, TexImageTarget target,
                            GLint level, GLenum internalFormat, GLint xOffset,
                            GLint yOffset, GLint zOffset, GLenum unpackFormat,
                            GLenum unpackType, dom::Element* elem,
                            ErrorResult* const out_error)
{
    auto sfer = mContext->SurfaceFromElement(elem);

    // While it's counter-intuitive, the shape of the SFEResult API means that we should
    // try to pull out a surface first, and then, if we do pull out a surface, check
    // CORS/write-only/etc..
    UniquePtr<webgl::TexUnpackBlob> blob;
    const bool isAlphaPremult = sfer.mIsPremultiplied;

    const auto& layersImage = sfer.mLayersImage;
    if (layersImage && !gfxPrefs::WebGLDisableDOMBlitUploads()) {
        blob.reset(new webgl::TexUnpackImage(layersImage, isAlphaPremult));
    } else if (sfer.GetSourceSurface()) {
        blob.reset(new webgl::TexUnpackSurface(sfer.GetSourceSurface(), isAlphaPremult));
    }

    if (blob) {
        if (!sfer.mCORSUsed) {
            auto& srcPrincipal = sfer.mPrincipal;
            nsIPrincipal* dstPrincipal = mContext->GetCanvas()->NodePrincipal();

            if (!dstPrincipal->Subsumes(srcPrincipal)) {
                mContext->GenerateWarning("%s: Cross-origin elements require CORS.",
                                          funcName);
                out_error->Throw(NS_ERROR_DOM_SECURITY_ERR);
                return;
            }
        }

        if (sfer.mIsWriteOnly) {
            // mIsWriteOnly defaults to true, and so will be true even if SFE merely
            // failed. Thus we must test mIsWriteOnly after successfully retrieving an
            // Image or SourceSurface.
            mContext->GenerateWarning("%s: Element is write-only, thus cannot be"
                                      " uploaded.",
                                      funcName);
            out_error->Throw(NS_ERROR_DOM_SECURITY_ERR);
            return;
        }
    } else {
        mContext->GenerateWarning("%s: Failed to get data from DOM element. Implicit"
                                  " width and height for this upload will be zero.",
                                  funcName);

        const uint32_t width = 0;
        const uint32_t height = 0;
        const uint32_t depth = 1; // Implicit depth for DOM uploads is always 1.
        const size_t byteCount = 0;
        blob.reset(new webgl::TexUnpackBytes(width, height, depth, byteCount, nullptr));
    }
    MOZ_ASSERT(blob);

    const GLint border = 0;
    TexOrSubImage(isSubImage, funcName, target, level, internalFormat, xOffset, yOffset,
                  zOffset, border, unpackFormat, unpackType, blob.get());
}


//////////////////////////////////////////////////////////////////////////////////////////

void
WebGLTexture::TexOrSubImage(bool isSubImage, const char* funcName, TexImageTarget target,
                            GLint level, GLenum internalFormat, GLint xOffset,
                            GLint yOffset, GLint zOffset, GLint border,
                            GLenum unpackFormat, GLenum unpackType,
                            webgl::TexUnpackBlob* blob)
{
    if (isSubImage) {
        TexSubImage(funcName, target, level, xOffset, yOffset, zOffset, unpackFormat,
                    unpackType, blob);
    } else {
        TexImage(funcName, target, level, internalFormat, border, unpackFormat,
                 unpackType, blob);
    }
}

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////

static bool
ValidateTexImage(WebGLContext* webgl, WebGLTexture* texture, const char* funcName,
                 TexImageTarget target, GLint level,
                 WebGLTexture::ImageInfo** const out_imageInfo)
{
    // Check level
    if (level < 0) {
        webgl->ErrorInvalidValue("%s: `level` must be >= 0.", funcName);
        return false;
    }

    if (level >= WebGLTexture::kMaxLevelCount) {
        webgl->ErrorInvalidValue("%s: `level` is too large.", funcName);
        return false;
    }

    WebGLTexture::ImageInfo& imageInfo = texture->ImageInfoAt(target, level);

    *out_imageInfo = &imageInfo;
    return true;
}


// For *TexImage*
bool
WebGLTexture::ValidateTexImageSpecification(const char* funcName, TexImageTarget target,
                                            GLint level, GLsizei width, GLsizei height,
                                            GLsizei depth, GLint border,
                                            WebGLTexture::ImageInfo** const out_imageInfo)
{
    if (mImmutable) {
        mContext->ErrorInvalidOperation("%s: Specified texture is immutable.", funcName);
        return false;
    }

    // Do this early to validate `level`.
    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImage(mContext, this, funcName, target, level, &imageInfo))
        return false;

    // Check border
    if (border != 0) {
        mContext->ErrorInvalidValue("%s: `border` must be 0.", funcName);
        return false;
    }

    if (width < 0 || height < 0 || depth < 0) {
        /* GL ES Version 2.0.25 - 3.7.1 Texture Image Specification
         *   "If wt and ht are the specified image width and height,
         *   and if either wt or ht are less than zero, then the error
         *   INVALID_VALUE is generated."
         */
        mContext->ErrorInvalidValue("%s: `width`/`height`/`depth` must be >= 0.",
                                    funcName);
        return false;
    }

    if (mTarget == LOCAL_GL_TEXTURE_CUBE_MAP &&
        width != height)
    {
        mContext->ErrorInvalidValue("%s: Cube map images must be square.", funcName);
        return false;
    }

    /* GLES 3.0.4, p133-134:
     * GL_MAX_TEXTURE_SIZE is *not* the max allowed texture size. Rather, it is the
     * max (width/height) size guaranteed not to generate an INVALID_VALUE for too-large
     * dimensions. Sizes larger than GL_MAX_TEXTURE_SIZE *may or may not* result in an
     * INVALID_VALUE, or possibly GL_OOM.
     *
     * However, we have needed to set our maximums lower in the past to prevent resource
     * corruption. Therefore we have mImplMaxTextureSize, which is neither necessarily
     * lower nor higher than MAX_TEXTURE_SIZE.
     *
     * Note that mImplMaxTextureSize must be >= than the advertized MAX_TEXTURE_SIZE.
     * For simplicity, we advertize MAX_TEXTURE_SIZE as mImplMaxTextureSize.
     */

    uint32_t maxWidthHeight = 0;
    uint32_t maxDepth = 0;

    MOZ_ASSERT(level <= 31);
    switch (target.get()) {
    case LOCAL_GL_TEXTURE_2D:
        maxWidthHeight = mContext->mImplMaxTextureSize >> level;
        maxDepth = 1;
        break;

    case LOCAL_GL_TEXTURE_3D:
        maxWidthHeight = mContext->mImplMax3DTextureSize >> level;
        maxDepth = maxWidthHeight;
        break;

    case LOCAL_GL_TEXTURE_2D_ARRAY:
        maxWidthHeight = mContext->mImplMaxTextureSize >> level;
        // "The maximum number of layers for two-dimensional array textures (depth)
        //  must be at least MAX_ARRAY_TEXTURE_LAYERS for all levels."
        maxDepth = mContext->mImplMaxArrayTextureLayers;
        break;

    default: // cube maps
        MOZ_ASSERT(IsCubeMap());
        maxWidthHeight = mContext->mImplMaxCubeMapTextureSize >> level;
        maxDepth = 1;
        break;
    }

    if (uint32_t(width) > maxWidthHeight ||
        uint32_t(height) > maxWidthHeight ||
        uint32_t(depth) > maxDepth)
    {
        mContext->ErrorInvalidValue("%s: Requested size at this level is unsupported.",
                                    funcName);
        return false;
    }

    {
        /* GL ES Version 2.0.25 - 3.7.1 Texture Image Specification
         *   "If level is greater than zero, and either width or
         *   height is not a power-of-two, the error INVALID_VALUE is
         *   generated."
         *
         * This restriction does not apply to GL ES Version 3.0+.
         */
        bool requirePOT = (!mContext->IsWebGL2() && level != 0);

        if (requirePOT) {
            if (!IsPowerOfTwo(uint32_t(width)) || !IsPowerOfTwo(uint32_t(height))) {
                mContext->ErrorInvalidValue("%s: For level > 0, width and height must be"
                                            " powers of two.",
                                            funcName);
                return false;
            }
        }
    }

    *out_imageInfo = imageInfo;
    return true;
}

// For *TexSubImage*
bool
WebGLTexture::ValidateTexImageSelection(const char* funcName, TexImageTarget target,
                                        GLint level, GLint xOffset, GLint yOffset,
                                        GLint zOffset, GLsizei width, GLsizei height,
                                        GLsizei depth,
                                        WebGLTexture::ImageInfo** const out_imageInfo)
{
    // The conformance test wants bad arg checks before imageInfo checks.
    if (xOffset < 0 || yOffset < 0 || zOffset < 0 ||
        width < 0 || height < 0 || depth < 0)
    {
        mContext->ErrorInvalidValue("%s: Offsets and dimensions must be >=0.", funcName);
        return false;
    }

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImage(mContext, this, funcName, target, level, &imageInfo))
        return false;

    if (!imageInfo->IsDefined()) {
        mContext->ErrorInvalidOperation("%s: The specified TexImage has not yet been"
                                        " specified.",
                                        funcName);
        return false;
    }

    const auto totalX = CheckedUint32(xOffset) + width;
    const auto totalY = CheckedUint32(yOffset) + height;
    const auto totalZ = CheckedUint32(zOffset) + depth;

    if (!totalX.isValid() || totalX.value() > imageInfo->mWidth ||
        !totalY.isValid() || totalY.value() > imageInfo->mHeight ||
        !totalZ.isValid() || totalZ.value() > imageInfo->mDepth)
    {
        mContext->ErrorInvalidValue("%s: Offset+size must be <= the size of the existing"
                                    " specified image.",
                                    funcName);
        return false;
    }

    *out_imageInfo = imageInfo;
    return true;
}

static bool
ValidateCompressedTexUnpack(WebGLContext* webgl, const char* funcName, GLsizei width,
                            GLsizei height, GLsizei depth,
                            const webgl::FormatInfo* format, size_t dataSize)
{
    auto compression = format->compression;

    auto bytesPerBlock = compression->bytesPerBlock;
    auto blockWidth = compression->blockWidth;
    auto blockHeight = compression->blockHeight;

    auto widthInBlocks = CheckedUint32(width) / blockWidth;
    auto heightInBlocks = CheckedUint32(height) / blockHeight;
    if (width % blockWidth) widthInBlocks += 1;
    if (height % blockHeight) heightInBlocks += 1;

    const CheckedUint32 blocksPerImage = widthInBlocks * heightInBlocks;
    const CheckedUint32 bytesPerImage = bytesPerBlock * blocksPerImage;
    const CheckedUint32 bytesNeeded = bytesPerImage * depth;

    if (!bytesNeeded.isValid()) {
        webgl->ErrorOutOfMemory("%s: Overflow while computing the needed buffer size.",
                                funcName);
        return false;
    }

    if (dataSize != bytesNeeded.value()) {
        webgl->ErrorInvalidValue("%s: Provided buffer's size must match expected size."
                                 " (needs %u, has %u)",
                                 funcName, bytesNeeded.value(), dataSize);
        return false;
    }

    return true;
}

static bool
DoChannelsMatchForCopyTexImage(const webgl::FormatInfo* srcFormat,
                               const webgl::FormatInfo* dstFormat)
{
    // GLES 3.0.4 p140 Table 3.16 "Valid CopyTexImage source framebuffer/destination
    // texture base internal format combinations."

    switch (srcFormat->unsizedFormat) {
    case webgl::UnsizedFormat::RGBA:
        switch (dstFormat->unsizedFormat) {
        case webgl::UnsizedFormat::A:
        case webgl::UnsizedFormat::L:
        case webgl::UnsizedFormat::LA:
        case webgl::UnsizedFormat::R:
        case webgl::UnsizedFormat::RG:
        case webgl::UnsizedFormat::RGB:
        case webgl::UnsizedFormat::RGBA:
            return true;
        default:
            return false;
        }

    case webgl::UnsizedFormat::RGB:
        switch (dstFormat->unsizedFormat) {
        case webgl::UnsizedFormat::L:
        case webgl::UnsizedFormat::R:
        case webgl::UnsizedFormat::RG:
        case webgl::UnsizedFormat::RGB:
            return true;
        default:
            return false;
        }

    case webgl::UnsizedFormat::RG:
        switch (dstFormat->unsizedFormat) {
        case webgl::UnsizedFormat::L:
        case webgl::UnsizedFormat::R:
        case webgl::UnsizedFormat::RG:
            return true;
        default:
            return false;
        }

    case webgl::UnsizedFormat::R:
        switch (dstFormat->unsizedFormat) {
        case webgl::UnsizedFormat::L:
        case webgl::UnsizedFormat::R:
            return true;
        default:
            return false;
        }

    default:
        return false;
    }
}

static bool
EnsureImageDataInitializedForUpload(WebGLTexture* tex, const char* funcName,
                                    TexImageTarget target, GLint level, GLint xOffset,
                                    GLint yOffset, GLint zOffset, uint32_t width,
                                    uint32_t height, uint32_t depth,
                                    WebGLTexture::ImageInfo* imageInfo,
                                    bool* const out_uploadWillInitialize)
{
    *out_uploadWillInitialize = false;

    if (!imageInfo->IsDataInitialized()) {
        const bool isFullUpload = (!xOffset && !yOffset && !zOffset &&
                                   width == imageInfo->mWidth &&
                                   height == imageInfo->mHeight &&
                                   depth == imageInfo->mDepth);
        if (isFullUpload) {
            *out_uploadWillInitialize = true;
        } else {
            WebGLContext* webgl = tex->mContext;
            webgl->GenerateWarning("%s: Texture has not been initialized prior to a"
                                   " partial upload, forcing the browser to clear it."
                                   " This may be slow.",
                                   funcName);
            if (!tex->InitializeImageData(funcName, target, level)) {
                MOZ_ASSERT(false, "Unexpected failure to init image data.");
                return false;
            }
        }
    }

    return true;
}

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// Actual calls

static inline GLenum
DoTexStorage(gl::GLContext* gl, TexTarget target, GLsizei levels, GLenum sizedFormat,
             GLsizei width, GLsizei height, GLsizei depth)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    switch (target.get()) {
    case LOCAL_GL_TEXTURE_2D:
    case LOCAL_GL_TEXTURE_CUBE_MAP:
        MOZ_ASSERT(depth == 1);
        gl->fTexStorage2D(target.get(), levels, sizedFormat, width, height);
        break;

    case LOCAL_GL_TEXTURE_3D:
    case LOCAL_GL_TEXTURE_2D_ARRAY:
        gl->fTexStorage3D(target.get(), levels, sizedFormat, width, height, depth);
        break;

    default:
        MOZ_CRASH("bad target");
    }

    return errorScope.GetError();
}

static bool
Is3D(TexImageTarget target)
{
    switch (target.get()) {
    case LOCAL_GL_TEXTURE_2D:
    case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X:
    case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_X:
    case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Y:
    case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Y:
    case LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_Z:
    case LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z:
        return false;

    case LOCAL_GL_TEXTURE_3D:
    case LOCAL_GL_TEXTURE_2D_ARRAY:
        return true;

    default:
        MOZ_CRASH("bad target");
    }
}

GLenum
DoTexImage(gl::GLContext* gl, TexImageTarget target, GLint level,
           const webgl::DriverUnpackInfo* dui, GLsizei width, GLsizei height,
           GLsizei depth, const void* data)
{
    const GLint border = 0;

    gl::GLContext::LocalErrorScope errorScope(*gl);

    if (Is3D(target)) {
        gl->fTexImage3D(target.get(), level, dui->internalFormat, width, height, depth,
                        border, dui->unpackFormat, dui->unpackType, data);
    } else {
        MOZ_ASSERT(depth == 1);
        gl->fTexImage2D(target.get(), level, dui->internalFormat, width, height, border,
                        dui->unpackFormat, dui->unpackType, data);
    }

    return errorScope.GetError();
}

GLenum
DoTexSubImage(gl::GLContext* gl, TexImageTarget target, GLint level, GLint xOffset,
              GLint yOffset, GLint zOffset, GLsizei width, GLsizei height, GLsizei depth,
              const webgl::PackingInfo& pi, const void* data)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    if (Is3D(target)) {
        gl->fTexSubImage3D(target.get(), level, xOffset, yOffset, zOffset, width, height,
                           depth, pi.format, pi.type, data);
    } else {
        MOZ_ASSERT(zOffset == 0);
        MOZ_ASSERT(depth == 1);
        gl->fTexSubImage2D(target.get(), level, xOffset, yOffset, width, height,
                           pi.format, pi.type, data);
    }

    return errorScope.GetError();
}

static inline GLenum
DoCompressedTexImage(gl::GLContext* gl, TexImageTarget target, GLint level,
                     GLenum internalFormat, GLsizei width, GLsizei height, GLsizei depth,
                     GLint border, GLsizei dataSize, const void* data)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    if (Is3D(target)) {
        gl->fCompressedTexImage3D(target.get(), level, internalFormat, width, height,
                                  depth, border, dataSize, data);
    } else {
        MOZ_ASSERT(depth == 1);
        gl->fCompressedTexImage2D(target.get(), level, internalFormat, width, height,
                                  border, dataSize, data);
    }

    return errorScope.GetError();
}

GLenum
DoCompressedTexSubImage(gl::GLContext* gl, TexImageTarget target, GLint level,
                        GLint xOffset, GLint yOffset, GLint zOffset, GLsizei width,
                        GLsizei height, GLsizei depth, GLenum sizedUnpackFormat,
                        GLsizei dataSize, const void* data)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    if (Is3D(target)) {
        gl->fCompressedTexSubImage3D(target.get(), level, xOffset, yOffset, zOffset,
                                     width, height, depth, sizedUnpackFormat, dataSize,
                                     data);
    } else {
        MOZ_ASSERT(zOffset == 0);
        MOZ_ASSERT(depth == 1);
        gl->fCompressedTexSubImage2D(target.get(), level, xOffset, yOffset, width,
                                     height, sizedUnpackFormat, dataSize, data);
    }

    return errorScope.GetError();
}

static inline GLenum
DoCopyTexImage2D(gl::GLContext* gl, TexImageTarget target, GLint level,
                 GLenum internalFormat, GLint x, GLint y, GLsizei width, GLsizei height,
                 GLint border)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    MOZ_ASSERT(!Is3D(target));
    gl->fCopyTexImage2D(target.get(), level, internalFormat, x, y, width, height,
                        border);

    return errorScope.GetError();
}

static inline GLenum
DoCopyTexSubImage(gl::GLContext* gl, TexImageTarget target, GLint level, GLint xOffset,
                  GLint yOffset, GLint zOffset, GLint x, GLint y, GLsizei width,
                  GLsizei height)
{
    gl::GLContext::LocalErrorScope errorScope(*gl);

    if (Is3D(target)) {
        gl->fCopyTexSubImage3D(target.get(), level, xOffset, yOffset, zOffset, x, y,
                               width, height);
    } else {
        MOZ_ASSERT(zOffset == 0);
        gl->fCopyTexSubImage2D(target.get(), level, xOffset, yOffset, x, y, width,
                               height);
    }

    return errorScope.GetError();
}

//////////////////////////////////////////////////////////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////
// Actual (mostly generic) function implementations

static bool
ValidateCompressedTexImageRestrictions(const char* funcName, WebGLContext* webgl,
                                       TexImageTarget target, uint32_t level,
                                       const webgl::FormatInfo* format, uint32_t width,
                                       uint32_t height, uint32_t depth)
{
    const auto fnIsDimValid_S3TC = [level](uint32_t size, uint32_t blockSize) {
        if (size % blockSize == 0)
            return true;

        if (level == 0)
            return false;

        return (size == 0 || size == 1 || size == 2);
    };

    switch (format->compression->family) {
    case webgl::CompressionFamily::PVRTC:
        if (!IsPowerOfTwo(width) || !IsPowerOfTwo(height)) {
            webgl->ErrorInvalidValue("%s: %s requires power-of-two width and height.",
                                     funcName, format->name);
            return false;
        }

        break;

    case webgl::CompressionFamily::S3TC:
        if (!fnIsDimValid_S3TC(width, format->compression->blockWidth) ||
            !fnIsDimValid_S3TC(height, format->compression->blockHeight))
        {
            webgl->ErrorInvalidOperation("%s: %s requires that width and height are"
                                         " block-aligned, or, if level>0, equal to 0, 1,"
                                         " or 2.",
                                         funcName, format->name);
            return false;
        }

        break;

    // Default: There are no restrictions on CompressedTexImage.
    default: // ATC, ETC1, ES3
        break;
    }

    return true;
}

static bool
ValidateTargetForFormat(const char* funcName, WebGLContext* webgl, TexImageTarget target,
                        const webgl::FormatInfo* format)
{
    // GLES 3.0.4 p127:
    // "Textures with a base internal format of DEPTH_COMPONENT or DEPTH_STENCIL are
    //  supported by texture image specification commands only if `target` is TEXTURE_2D,
    //  TEXTURE_2D_ARRAY, or TEXTURE_CUBE_MAP. Using these formats in conjunction with any
    //  other `target` will result in an INVALID_OPERATION error."

    switch (format->effectiveFormat) {
    // TEXTURE_2D_ARRAY but not TEXTURE_3D:
    // D and DS formats
    case webgl::EffectiveFormat::DEPTH_COMPONENT16:
    case webgl::EffectiveFormat::DEPTH_COMPONENT24:
    case webgl::EffectiveFormat::DEPTH_COMPONENT32F:
    case webgl::EffectiveFormat::DEPTH24_STENCIL8:
    case webgl::EffectiveFormat::DEPTH32F_STENCIL8:
    // CompressionFamily::ES3
    case webgl::EffectiveFormat::COMPRESSED_R11_EAC:
    case webgl::EffectiveFormat::COMPRESSED_SIGNED_R11_EAC:
    case webgl::EffectiveFormat::COMPRESSED_RG11_EAC:
    case webgl::EffectiveFormat::COMPRESSED_SIGNED_RG11_EAC:
    case webgl::EffectiveFormat::COMPRESSED_RGB8_ETC2:
    case webgl::EffectiveFormat::COMPRESSED_SRGB8_ETC2:
    case webgl::EffectiveFormat::COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2:
    case webgl::EffectiveFormat::COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2:
    case webgl::EffectiveFormat::COMPRESSED_RGBA8_ETC2_EAC:
    case webgl::EffectiveFormat::COMPRESSED_SRGB8_ALPHA8_ETC2_EAC:
    // CompressionFamily::S3TC
    case webgl::EffectiveFormat::COMPRESSED_RGB_S3TC_DXT1_EXT:
    case webgl::EffectiveFormat::COMPRESSED_RGBA_S3TC_DXT1_EXT:
    case webgl::EffectiveFormat::COMPRESSED_RGBA_S3TC_DXT3_EXT:
    case webgl::EffectiveFormat::COMPRESSED_RGBA_S3TC_DXT5_EXT:
        if (target == LOCAL_GL_TEXTURE_3D) {
            webgl->ErrorInvalidOperation("%s: Format %s cannot be used with TEXTURE_3D.",
                                         funcName, format->name);
            return false;
        }
        break;

    // No 3D targets:
    // CompressionFamily::ATC
    case webgl::EffectiveFormat::ATC_RGB_AMD:
    case webgl::EffectiveFormat::ATC_RGBA_EXPLICIT_ALPHA_AMD:
    case webgl::EffectiveFormat::ATC_RGBA_INTERPOLATED_ALPHA_AMD:
    // CompressionFamily::PVRTC
    case webgl::EffectiveFormat::COMPRESSED_RGB_PVRTC_4BPPV1:
    case webgl::EffectiveFormat::COMPRESSED_RGBA_PVRTC_4BPPV1:
    case webgl::EffectiveFormat::COMPRESSED_RGB_PVRTC_2BPPV1:
    case webgl::EffectiveFormat::COMPRESSED_RGBA_PVRTC_2BPPV1:
    // CompressionFamily::ETC1
    case webgl::EffectiveFormat::ETC1_RGB8_OES:
        if (target == LOCAL_GL_TEXTURE_3D ||
            target == LOCAL_GL_TEXTURE_2D_ARRAY)
        {
            webgl->ErrorInvalidOperation("%s: Format %s cannot be used with TEXTURE_3D or"
                                         " TEXTURE_2D_ARRAY.",
                                         funcName, format->name);
            return false;
        }
        break;

    default:
        break;
    }

    return true;
}

void
WebGLTexture::TexStorage(const char* funcName, TexTarget target, GLsizei levels,
                         GLenum sizedFormat, GLsizei width, GLsizei height, GLsizei depth)
{
    // Check levels
    if (levels < 1) {
        mContext->ErrorInvalidValue("%s: `levels` must be >= 1.", funcName);
        return;
    }

    if (!width || !height || !depth) {
        mContext->ErrorInvalidValue("%s: Dimensions must be non-zero.", funcName);
        return;
    }

    const TexImageTarget testTarget = IsCubeMap() ? LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X
                                                  : target.get();
    const GLint testLevel = 0;
    const GLint border = 0;

    WebGLTexture::ImageInfo* testImageInfo;
    if (!ValidateTexImageSpecification(funcName, testTarget, testLevel, width, height,
                                       depth, border, &testImageInfo))
    {
        return;
    }
    MOZ_ASSERT(testImageInfo);
    mozilla::Unused << testImageInfo;

    auto dstUsage = mContext->mFormatUsage->GetSizedTexUsage(sizedFormat);
    if (!dstUsage) {
        mContext->ErrorInvalidEnum("%s: Invalid internalformat: 0x%04x", funcName,
                                   sizedFormat);
        return;
    }
    auto dstFormat = dstUsage->format;

    if (!ValidateTargetForFormat(funcName, mContext, testTarget, dstFormat))
        return;

    if (dstFormat->compression) {
        if (!ValidateCompressedTexImageRestrictions(funcName, mContext, testTarget,
                                                    testLevel, dstFormat, width, height,
                                                    depth))
        {
            return;
        }
    }

    ////////////////////////////////////

    const auto lastLevel = levels - 1;
    MOZ_ASSERT(lastLevel <= 31, "Right-shift is only defined for bits-1.");

    const uint32_t lastLevelWidth = uint32_t(width) >> lastLevel;
    const uint32_t lastLevelHeight = uint32_t(height) >> lastLevel;
    const uint32_t lastLevelDepth = uint32_t(depth) >> lastLevel;

    // If these are all zero, then some earlier level was the final 1x1x1 level.
    if (!lastLevelWidth && !lastLevelHeight && !lastLevelDepth) {
        mContext->ErrorInvalidOperation("%s: Too many levels requested for the given"
                                        " dimensions. (levels: %u, width: %u, height: %u,"
                                        " depth: %u)",
                                        funcName, levels, width, height, depth);
        return;
    }

    ////////////////////////////////////
    // Do the thing!

    mContext->gl->MakeCurrent();

    GLenum error = DoTexStorage(mContext->gl, target.get(), levels, sizedFormat, width,
                                height, depth);

    if (error == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Ran out of memory during texture allocation.",
                                   funcName);
        return;
    }
    if (error) {
        MOZ_RELEASE_ASSERT(false, "We should have caught all other errors.");
        mContext->ErrorInvalidOperation("%s: Unexpected error during texture allocation.",
                                        funcName);
        return;
    }

    ////////////////////////////////////
    // Update our specification data.

    const bool isDataInitialized = false;
    const WebGLTexture::ImageInfo newInfo(dstUsage, width, height, depth,
                                          isDataInitialized);
    SetImageInfosAtLevel(0, newInfo);

    PopulateMipChain(0, levels-1);

    mImmutable = true;
}

////////////////////////////////////////
// Tex(Sub)Image

void
WebGLTexture::TexImage(const char* funcName, TexImageTarget target, GLint level,
                       GLenum internalFormat, GLint border, GLenum unpackFormat,
                       GLenum unpackType, webgl::TexUnpackBlob* blob)
{
    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSpecification(funcName, target, level, blob->mWidth,
                                       blob->mHeight, blob->mDepth, border, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    const webgl::PackingInfo srcPacking = { unpackFormat, unpackType };

    const auto& fua = mContext->mFormatUsage;
    auto dstUsage = fua->GetSizedTexUsage(internalFormat);
    if (!dstUsage) {
        if (internalFormat != unpackFormat) {
            mContext->ErrorInvalidOperation("%s: Unsized internalFormat must match"
                                            " unpack format.",
                                            funcName);
            return;
        }

        dstUsage = fua->GetUnsizedTexUsage(srcPacking);
    }

    if (!dstUsage) {
        if (!mContext->IsWebGL2()) {
            if (!fua->IsInternalFormatEnumValid(internalFormat)) {
                mContext->ErrorInvalidValue("%s: Invalid internalformat: 0x%04x",
                                            funcName, internalFormat);
                return;
            }

            if (!fua->AreUnpackEnumsValid(unpackFormat, unpackType)) {
                mContext->ErrorInvalidEnum("%s: Invalid unpack format/type:"
                                           " 0x%04x/0x%04x",
                                           funcName, unpackFormat, unpackType);
                return;
            }
        }

        mContext->ErrorInvalidOperation("%s: Invalid internalformat/format/type:"
                                        " 0x%04x/0x%04x/0x%04x",
                                        funcName, internalFormat, unpackFormat,
                                        unpackType);
        return;
    }

    const webgl::DriverUnpackInfo* driverUnpackInfo;
    if (!dstUsage->IsUnpackValid(srcPacking, &driverUnpackInfo)) {
        mContext->ErrorInvalidOperation("%s: Mismatched internalFormat and format/type:"
                                        " 0x%04x and 0x%04x/0x%04x",
                                        funcName, internalFormat, unpackFormat,
                                        unpackType);
        return;
    }

    ////////////////////////////////////
    // Get source info
    const bool isFunc3D = Is3D(target);
    if (!blob->ValidateUnpack(mContext, funcName, isFunc3D, srcPacking))
        return;

    ////////////////////////////////////
    // Check that source and dest info are compatible
    auto dstFormat = dstUsage->format;

    if (!ValidateTargetForFormat(funcName, mContext, target, dstFormat))
        return;

    if (!mContext->IsWebGL2() && dstFormat->hasDepth) {
        if (target != LOCAL_GL_TEXTURE_2D ||
            blob->mHasData ||
            level != 0)
        {
            mContext->ErrorInvalidOperation("%s: With format %s, this function may only"
                                            " be called with target=TEXTURE_2D,"
                                            " data=null, and level=0.",
                                            funcName, dstFormat->name);
            return;
        }
    }

    ////////////////////////////////////
    // Do the thing!

    MOZ_ALWAYS_TRUE( mContext->gl->MakeCurrent() );
    MOZ_ASSERT(mContext->gl->IsCurrent());

    // It's tempting to do allocation first, and TexSubImage second, but this is generally
    // slower.

    const ImageInfo newImageInfo(dstUsage, blob->mWidth, blob->mHeight, blob->mDepth,
                                 blob->mHasData);

    const bool isSubImage = false;
    const bool needsRespec = (imageInfo->mWidth  != newImageInfo.mWidth ||
                              imageInfo->mHeight != newImageInfo.mHeight ||
                              imageInfo->mDepth  != newImageInfo.mDepth ||
                              imageInfo->mFormat != newImageInfo.mFormat);
    const GLint xOffset = 0;
    const GLint yOffset = 0;
    const GLint zOffset = 0;

    GLenum glError;
    blob->TexOrSubImage(isSubImage, needsRespec, funcName, this, target, level,
                        driverUnpackInfo, xOffset, yOffset, zOffset, &glError);

    if (glError == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Driver ran out of memory during upload.",
                                   funcName);
        return;
    }

    if (glError) {
        mContext->ErrorInvalidOperation("%s: Unexpected error during upload: 0x%04x",
                                        funcName, glError);
        printf_stderr("%s: dui: %x/%x/%x\n", funcName, driverUnpackInfo->internalFormat,
                      driverUnpackInfo->unpackFormat, driverUnpackInfo->unpackType);
        MOZ_ASSERT(false, "Unexpected GL error.");
        return;
    }

    ////////////////////////////////////
    // Update our specification data.

    SetImageInfo(imageInfo, newImageInfo);
}

void
WebGLTexture::TexSubImage(const char* funcName, TexImageTarget target, GLint level,
                          GLint xOffset, GLint yOffset, GLint zOffset,
                          GLenum unpackFormat, GLenum unpackType,
                          webgl::TexUnpackBlob* blob)
{
    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSelection(funcName, target, level, xOffset, yOffset, zOffset,
                                   blob->mWidth, blob->mHeight, blob->mDepth, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    auto dstUsage = imageInfo->mFormat;
    auto dstFormat = dstUsage->format;

    if (dstFormat->compression) {
        mContext->ErrorInvalidEnum("%s: Specified TexImage must not be compressed.",
                                   funcName);
        return;
    }

    if (!mContext->IsWebGL2() && dstFormat->hasDepth) {
        mContext->ErrorInvalidOperation("%s: Function may not be called on a texture of"
                                        " format %s.",
                                        funcName, dstFormat->name);
        return;
    }

    ////////////////////////////////////
    // Get source info

    const webgl::PackingInfo srcPacking = { unpackFormat, unpackType };
    const webgl::DriverUnpackInfo* driverUnpackInfo;
    if (!dstUsage->IsUnpackValid(srcPacking, &driverUnpackInfo)) {
        if (!mContext->IsWebGL2()) {
            const auto& fua = mContext->mFormatUsage;
            if (!fua->AreUnpackEnumsValid(unpackFormat, unpackType)) {
                mContext->ErrorInvalidEnum("%s: Invalid unpack format/type:"
                                           " 0x%04x/0x%04x",
                                           funcName, unpackFormat, unpackType);
                return;
            }
        }

        mContext->ErrorInvalidOperation("%s: Mismatched internalFormat and format/type:"
                                        " %s and 0x%04x/0x%04x",
                                        funcName, dstFormat->name, unpackFormat,
                                        unpackType);
        return;
    }

    const bool isFunc3D = Is3D(target);
    if (!blob->ValidateUnpack(mContext, funcName, isFunc3D, srcPacking))
        return;

    ////////////////////////////////////
    // Do the thing!

    mContext->gl->MakeCurrent();

    bool uploadWillInitialize;
    if (!EnsureImageDataInitializedForUpload(this, funcName, target, level, xOffset,
                                             yOffset, zOffset, blob->mWidth,
                                             blob->mHeight, blob->mDepth, imageInfo,
                                             &uploadWillInitialize))
    {
        return;
    }

    const bool isSubImage = true;
    const bool needsRespec = false;

    GLenum glError;
    blob->TexOrSubImage(isSubImage, needsRespec, funcName, this, target, level,
                        driverUnpackInfo, xOffset, yOffset, zOffset, &glError);

    if (glError == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Driver ran out of memory during upload.",
                                   funcName);
        return;
    }

    if (glError) {
        mContext->ErrorInvalidOperation("%s: Unexpected error during upload: 0x04x",
                                        funcName, glError);
        MOZ_ASSERT(false, "Unexpected GL error.");
        return;
    }

    ////////////////////////////////////
    // Update our specification data?

    if (uploadWillInitialize) {
        imageInfo->SetIsDataInitialized(true, this);
    }
}

////////////////////////////////////////
// CompressedTex(Sub)Image

void
WebGLTexture::CompressedTexImage(const char* funcName, TexImageTarget target, GLint level,
                                 GLenum internalFormat, GLsizei width, GLsizei height,
                                 GLsizei depth, GLint border,
                                 const dom::ArrayBufferView& view)
{
    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSpecification(funcName, target, level, width, height, depth,
                                       border, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    auto usage = mContext->mFormatUsage->GetSizedTexUsage(internalFormat);
    if (!usage) {
        mContext->ErrorInvalidEnum("%s: Invalid internalFormat: 0x%04x", funcName,
                                   internalFormat);
        return;
    }

    auto format = usage->format;
    if (!format->compression) {
        mContext->ErrorInvalidEnum("%s: Specified internalFormat must be compressed.",
                                   funcName);
        return;
    }

    if (!ValidateTargetForFormat(funcName, mContext, target, format))
        return;

    ////////////////////////////////////
    // Get source info

    view.ComputeLengthAndData();
    const void* data = view.DataAllowShared();
    size_t dataSize = view.LengthAllowShared();

    if (!ValidateCompressedTexUnpack(mContext, funcName, width, height, depth, format,
                                     dataSize))
    {
        return;
    }

    ////////////////////////////////////
    // Check that source is compatible with dest

    if (!ValidateCompressedTexImageRestrictions(funcName, mContext, target, level, format,
                                                width, height, depth))
    {
        return;
    }

    ////////////////////////////////////
    // Do the thing!

    mContext->gl->MakeCurrent();

    // Warning: Possibly shared memory.  See bug 1225033.
    GLenum error = DoCompressedTexImage(mContext->gl, target, level, internalFormat,
                                        width, height, depth, border, dataSize, data);
    if (error == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Ran out of memory during upload.", funcName);
        return;
    }
    if (error) {
        MOZ_RELEASE_ASSERT(false, "We should have caught all other errors.");
        mContext->GenerateWarning("%s: Unexpected error during texture upload. Context"
                                  " lost.",
                                  funcName);
        mContext->ForceLoseContext();
        return;
    }

    ////////////////////////////////////
    // Update our specification data.

    const bool isDataInitialized = true;
    const ImageInfo newImageInfo(usage, width, height, depth, isDataInitialized);
    SetImageInfo(imageInfo, newImageInfo);
}

static inline bool
IsSubImageBlockAligned(const webgl::CompressedFormatInfo* compression,
                       const WebGLTexture::ImageInfo* imageInfo, GLint xOffset,
                       GLint yOffset, uint32_t width, uint32_t height)
{
    if (xOffset % compression->blockWidth != 0 ||
        yOffset % compression->blockHeight != 0)
    {
        return false;
    }

    if (width % compression->blockWidth != 0 && xOffset + width != imageInfo->mWidth)
        return false;

    if (height % compression->blockHeight != 0 && yOffset + height != imageInfo->mHeight)
        return false;

    return true;
}

void
WebGLTexture::CompressedTexSubImage(const char* funcName, TexImageTarget target,
                                    GLint level, GLint xOffset, GLint yOffset,
                                    GLint zOffset, GLsizei width, GLsizei height,
                                    GLsizei depth, GLenum sizedUnpackFormat,
                                    const dom::ArrayBufferView& view)
{
    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSelection(funcName, target, level, xOffset, yOffset, zOffset,
                                   width, height, depth, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    auto dstUsage = imageInfo->mFormat;
    auto dstFormat = dstUsage->format;

    ////////////////////////////////////
    // Get source info

    view.ComputeLengthAndData();
    size_t dataSize = view.LengthAllowShared();
    const void* data = view.DataAllowShared();

    auto srcUsage = mContext->mFormatUsage->GetSizedTexUsage(sizedUnpackFormat);
    if (!srcUsage->format->compression) {
        mContext->ErrorInvalidEnum("%s: Specified format must be compressed.", funcName);
        return;
    }

    if (srcUsage != dstUsage) {
        mContext->ErrorInvalidOperation("%s: `format` must match the format of the"
                                        " existing texture image.",
                                        funcName);
        return;
    }

    auto format = srcUsage->format;
    MOZ_ASSERT(format == dstFormat);
    if (!ValidateCompressedTexUnpack(mContext, funcName, width, height, depth, format,
                                     dataSize))
    {
        return;
    }

    ////////////////////////////////////
    // Check that source is compatible with dest

    switch (format->compression->family) {
    // Forbidden:
    case webgl::CompressionFamily::ETC1:
    case webgl::CompressionFamily::ATC:
        mContext->ErrorInvalidOperation("%s: Format does not allow sub-image"
                                        " updates.", funcName);
        return;

    // Block-aligned:
    case webgl::CompressionFamily::ES3:  // Yes, the ES3 formats don't match the ES3
    case webgl::CompressionFamily::S3TC: // default behavior.
        if (!IsSubImageBlockAligned(dstFormat->compression, imageInfo, xOffset, yOffset,
                                    width, height))
        {
            mContext->ErrorInvalidOperation("%s: Format requires block-aligned sub-image"
                                            " updates.",
                                            funcName);
            return;
        }
        break;

    // Full-only: (The ES3 default)
    default: // PVRTC
        if (xOffset || yOffset ||
            uint32_t(width) != imageInfo->mWidth ||
            uint32_t(height) != imageInfo->mHeight)
        {
            mContext->ErrorInvalidOperation("%s: Format does not allow partial sub-image"
                                            " updates.",
                                            funcName);
            return;
        }
        break;
    }

    ////////////////////////////////////
    // Do the thing!

    mContext->gl->MakeCurrent();

    bool uploadWillInitialize;
    if (!EnsureImageDataInitializedForUpload(this, funcName, target, level, xOffset,
                                             yOffset, zOffset, width, height, depth,
                                             imageInfo, &uploadWillInitialize))
    {
        return;
    }

    // Warning: Possibly shared memory.  See bug 1225033.
    GLenum error = DoCompressedTexSubImage(mContext->gl, target, level, xOffset, yOffset,
                                           zOffset, width, height, depth,
                                           sizedUnpackFormat, dataSize, data);
    if (error == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Ran out of memory during upload.", funcName);
        return;
    }
    if (error) {
        MOZ_RELEASE_ASSERT(false, "We should have caught all other errors.");
        mContext->GenerateWarning("%s: Unexpected error during texture upload. Context"
                                  " lost.",
                                  funcName);
        mContext->ForceLoseContext();
        return;
    }

    ////////////////////////////////////
    // Update our specification data?

    if (uploadWillInitialize) {
        imageInfo->SetIsDataInitialized(true, this);
    }
}

////////////////////////////////////////
// CopyTex(Sub)Image

static bool
ValidateCopyTexImageFormats(WebGLContext* webgl, const char* funcName,
                            const webgl::FormatInfo* srcFormat,
                            const webgl::FormatInfo* dstFormat)
{
    MOZ_ASSERT(!srcFormat->compression);
    if (dstFormat->compression) {
        webgl->ErrorInvalidEnum("%s: Specified destination must not have a compressed"
                                " format.",
                                funcName);
        return false;
    }

    if (dstFormat->effectiveFormat == webgl::EffectiveFormat::RGB9_E5) {
        webgl->ErrorInvalidOperation("%s: RGB9_E5 is an invalid destination for"
                                     " CopyTex(Sub)Image. (GLES 3.0.4 p145)",
                                     funcName);
        return false;
    }

    if (!DoChannelsMatchForCopyTexImage(srcFormat, dstFormat)) {
        webgl->ErrorInvalidOperation("%s: Destination channels must be compatible with"
                                     " source channels. (GLES 3.0.4 p140 Table 3.16)",
                                     funcName);
        return false;
    }

    return true;
}

////////////////////////////////////////////////////////////////////////////////

class ScopedCopyTexImageSource
{
    WebGLContext* const mWebGL;
    GLuint mRB;
    GLuint mFB;

public:
    ScopedCopyTexImageSource(WebGLContext* webgl, const char* funcName, uint32_t srcWidth,
                             uint32_t srcHeight, const webgl::FormatInfo* srcFormat,
                             const webgl::FormatUsageInfo* dstUsage);
    ~ScopedCopyTexImageSource();
};

ScopedCopyTexImageSource::ScopedCopyTexImageSource(WebGLContext* webgl,
                                                   const char* funcName,
                                                   uint32_t srcWidth, uint32_t srcHeight,
                                                   const webgl::FormatInfo* srcFormat,
                                                   const webgl::FormatUsageInfo* dstUsage)
    : mWebGL(webgl)
    , mRB(0)
    , mFB(0)
{
    switch (dstUsage->format->unsizedFormat) {
    case webgl::UnsizedFormat::L:
    case webgl::UnsizedFormat::A:
    case webgl::UnsizedFormat::LA:
        webgl->GenerateWarning("%s: Copying to a LUMINANCE, ALPHA, or LUMINANCE_ALPHA"
                               " is deprecated, and has severely reduced performance"
                               " on some platforms.",
                               funcName);
        break;

    default:
        MOZ_ASSERT(!dstUsage->textureSwizzleRGBA);
        return;
    }

    if (!dstUsage->textureSwizzleRGBA)
        return;

    gl::GLContext* gl = webgl->gl;

    GLenum sizedFormat;

    switch (srcFormat->componentType) {
    case webgl::ComponentType::NormUInt:
        sizedFormat = LOCAL_GL_RGBA8;
        break;

    case webgl::ComponentType::Float:
        if (webgl->IsExtensionEnabled(WebGLExtensionID::WEBGL_color_buffer_float)) {
            sizedFormat = LOCAL_GL_RGBA32F;
            break;
        }

        if (webgl->IsExtensionEnabled(WebGLExtensionID::EXT_color_buffer_half_float)) {
            sizedFormat = LOCAL_GL_RGBA16F;
            break;
        }
        MOZ_CRASH("Should be able to request CopyTexImage from Float.");

    default:
        MOZ_CRASH("Should be able to request CopyTexImage from this type.");
    }

    gl::ScopedTexture scopedTex(gl);
    gl::ScopedBindTexture scopedBindTex(gl, scopedTex.Texture(), LOCAL_GL_TEXTURE_2D);

    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, LOCAL_GL_NEAREST);
    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, LOCAL_GL_NEAREST);

    GLint blitSwizzle[4] = {LOCAL_GL_ZERO};
    switch (dstUsage->format->unsizedFormat) {
    case webgl::UnsizedFormat::L:
        blitSwizzle[0] = LOCAL_GL_RED;
        break;

    case webgl::UnsizedFormat::A:
        blitSwizzle[0] = LOCAL_GL_ALPHA;
        break;

    case webgl::UnsizedFormat::LA:
        blitSwizzle[0] = LOCAL_GL_RED;
        blitSwizzle[1] = LOCAL_GL_ALPHA;
        break;

    default:
        MOZ_CRASH("Unhandled unsizedFormat.");
    }

    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_SWIZZLE_R, blitSwizzle[0]);
    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_SWIZZLE_G, blitSwizzle[1]);
    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_SWIZZLE_B, blitSwizzle[2]);
    gl->fTexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_SWIZZLE_A, blitSwizzle[3]);

    gl->fCopyTexImage2D(LOCAL_GL_TEXTURE_2D, 0, sizedFormat, 0, 0, srcWidth,
                        srcHeight, 0);

    // Now create the swizzled FB we'll be exposing.

    GLuint rgbaRB = 0;
    gl->fGenRenderbuffers(1, &rgbaRB);
    gl::ScopedBindRenderbuffer scopedRB(gl, rgbaRB);
    gl->fRenderbufferStorage(LOCAL_GL_RENDERBUFFER, sizedFormat, srcWidth, srcHeight);

    GLuint rgbaFB = 0;
    gl->fGenFramebuffers(1, &rgbaFB);
    gl->fBindFramebuffer(LOCAL_GL_FRAMEBUFFER, rgbaFB);
    gl->fFramebufferRenderbuffer(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
                                 LOCAL_GL_RENDERBUFFER, rgbaRB);

    const GLenum status = gl->fCheckFramebufferStatus(LOCAL_GL_FRAMEBUFFER);
    if (status != LOCAL_GL_FRAMEBUFFER_COMPLETE) {
        MOZ_CRASH("Temp framebuffer is not complete.");
    }

    // Restore RB binding.
    scopedRB.Unwrap(); // This function should really have a better name.

    // Draw-blit rgbaTex into rgbaFB.
    const gfx::IntSize srcSize(srcWidth, srcHeight);
    gl->BlitHelper()->DrawBlitTextureToFramebuffer(scopedTex.Texture(), rgbaFB,
                                                   srcSize, srcSize);

    // Restore Tex2D binding and destroy the temp tex.
    scopedBindTex.Unwrap();
    scopedTex.Unwrap();

    // Leave RB and FB alive, and FB bound.
    mRB = rgbaRB;
    mFB = rgbaFB;
}

template<typename T>
static inline GLenum
ToGLHandle(const T& obj)
{
    return (obj ? obj->mGLName : 0);
}

ScopedCopyTexImageSource::~ScopedCopyTexImageSource()
{
    if (!mFB) {
        MOZ_ASSERT(!mRB);
        return;
    }
    MOZ_ASSERT(mRB);

    gl::GLContext* gl = mWebGL->gl;

    // If we're swizzling, it's because we're on a GL core (3.2+) profile, which has
    // split framebuffer support.
    gl->fBindFramebuffer(LOCAL_GL_DRAW_FRAMEBUFFER,
                         ToGLHandle(mWebGL->mBoundDrawFramebuffer));
    gl->fBindFramebuffer(LOCAL_GL_READ_FRAMEBUFFER,
                         ToGLHandle(mWebGL->mBoundReadFramebuffer));

    gl->fDeleteFramebuffers(1, &mFB);
    gl->fDeleteRenderbuffers(1, &mRB);
}

////////////////////////////////////////////////////////////////////////////////

// There is no CopyTexImage3D.
void
WebGLTexture::CopyTexImage2D(TexImageTarget target, GLint level, GLenum internalFormat,
                             GLint x, GLint y, GLsizei width, GLsizei height,
                             GLint border)
{
    const char funcName[] = "CopyTexImage2D";

    const uint8_t depth = 1;

    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSpecification(funcName, target, level, width, height, depth,
                                       border, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    ////////////////////////////////////
    // Get source info

    const webgl::FormatUsageInfo* srcUsage;
    uint32_t srcWidth;
    uint32_t srcHeight;
    if (!mContext->ValidateCurFBForRead(funcName, &srcUsage, &srcWidth, &srcHeight))
        return;
    auto srcFormat = srcUsage->format;

    ////////////////////////////////////
    // Check that source and dest info are compatible

    const auto& fua = mContext->mFormatUsage;

    auto dstUsage = fua->GetSizedTexUsage(internalFormat);
    if (!dstUsage) {
        // It must be an unsized format then...
        webgl::PackingInfo pi = {internalFormat, 0};

        switch (srcFormat->componentType) {
        case webgl::ComponentType::NormUInt:
            pi.type = LOCAL_GL_UNSIGNED_BYTE;
            break;

        case webgl::ComponentType::Float:
            pi.type = LOCAL_GL_FLOAT;
            break;

        default:
            break;
        }

        dstUsage = fua->GetUnsizedTexUsage(pi);
    }

    if (!dstUsage) {
        mContext->ErrorInvalidEnum("%s: Invalid internalFormat 0x%04x for FB format %s.",
                                   funcName, internalFormat, srcFormat->name);
        return;
    }
    auto dstFormat = dstUsage->format;

    if (!ValidateTargetForFormat(funcName, mContext, target, dstFormat))
        return;

    if (!mContext->IsWebGL2() && dstFormat->hasDepth) {
        mContext->ErrorInvalidOperation("%s: Function may not be called with format %s.",
                                        funcName, dstFormat->name);
        return;
    }

    if (!ValidateCopyTexImageFormats(mContext, funcName, srcFormat, dstFormat))
        return;

    ////////////////////////////////////
    // Do the thing!

    gl::GLContext* gl = mContext->gl;
    gl->MakeCurrent();

    ScopedCopyTexImageSource maybeSwizzle(mContext, funcName, srcWidth, srcHeight,
                                          srcFormat, dstUsage);

    uint32_t readX, readY;
    uint32_t writeX, writeY;
    uint32_t rwWidth, rwHeight;
    Intersect(srcWidth, x, width, &readX, &writeX, &rwWidth);
    Intersect(srcHeight, y, height, &readY, &writeY, &rwHeight);

    GLenum error;
    if (rwWidth == uint32_t(width) && rwHeight == uint32_t(height)) {
        error = DoCopyTexImage2D(gl, target, level, internalFormat, x, y, width, height,
                                 border);
    } else {
        // 1. Zero the texture data.
        // 2. CopyTexSubImage the subrect.

        const bool respecifyTexture = true;
        const uint8_t zOffset = 0;
        if (!ZeroTextureData(mContext, funcName, respecifyTexture, mGLName, target, level,
                             dstUsage, 0, 0, zOffset, width, height, depth))
        {
            mContext->ErrorOutOfMemory("%s: Failed to zero texture data.", funcName);
            MOZ_ASSERT(false, "Failed to zero texture data.");
            return;
        }

        if (!rwWidth || !rwHeight) {
            // There aren't any, so we're 'done'.
            mContext->DummyReadFramebufferOperation(funcName);
            return;
        }

        error = DoCopyTexSubImage(gl, target, level, writeX, writeY, zOffset, readX,
                                  readY, rwWidth, rwHeight);
    }

    if (error == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Ran out of memory during texture copy.",
                                   funcName);
        return;
    }
    if (error) {
        MOZ_RELEASE_ASSERT(false, "We should have caught all other errors.");
        mContext->GenerateWarning("%s: Unexpected error during texture copy. Context"
                                  " lost.",
                                  funcName);
        mContext->ForceLoseContext();
        return;
    }

    ////////////////////////////////////
    // Update our specification data.

    const bool isDataInitialized = true;
    const ImageInfo newImageInfo(dstUsage, width, height, depth, isDataInitialized);
    SetImageInfo(imageInfo, newImageInfo);
}

void
WebGLTexture::CopyTexSubImage(const char* funcName, TexImageTarget target, GLint level,
                              GLint xOffset, GLint yOffset, GLint zOffset, GLint x,
                              GLint y, GLsizei width, GLsizei height)
{
    const GLsizei depth = 1;

    ////////////////////////////////////
    // Get dest info

    WebGLTexture::ImageInfo* imageInfo;
    if (!ValidateTexImageSelection(funcName, target, level, xOffset, yOffset, zOffset,
                                   width, height, depth, &imageInfo))
    {
        return;
    }
    MOZ_ASSERT(imageInfo);

    auto dstUsage = imageInfo->mFormat;
    MOZ_ASSERT(dstUsage);
    auto dstFormat = dstUsage->format;

    if (!mContext->IsWebGL2() && dstFormat->hasDepth) {
        mContext->ErrorInvalidOperation("%s: Function may not be called on a texture of"
                                        " format %s.",
                                        funcName, dstFormat->name);
        return;
    }

    ////////////////////////////////////
    // Get source info

    const webgl::FormatUsageInfo* srcUsage;
    uint32_t srcWidth;
    uint32_t srcHeight;
    if (!mContext->ValidateCurFBForRead(funcName, &srcUsage, &srcWidth, &srcHeight))
        return;
    auto srcFormat = srcUsage->format;

    ////////////////////////////////////
    // Check that source and dest info are compatible

    if (!ValidateCopyTexImageFormats(mContext, funcName, srcFormat, dstFormat))
        return;

    ////////////////////////////////////
    // Do the thing!

    mContext->gl->MakeCurrent();

    ScopedCopyTexImageSource maybeSwizzle(mContext, funcName, srcWidth, srcHeight,
                                          srcFormat, dstUsage);

    uint32_t readX, readY;
    uint32_t writeX, writeY;
    uint32_t rwWidth, rwHeight;
    Intersect(srcWidth, x, width, &readX, &writeX, &rwWidth);
    Intersect(srcHeight, y, height, &readY, &writeY, &rwHeight);

    if (!rwWidth || !rwHeight) {
        // There aren't any, so we're 'done'.
        mContext->DummyReadFramebufferOperation(funcName);
        return;
    }

    bool uploadWillInitialize;
    if (!EnsureImageDataInitializedForUpload(this, funcName, target, level, xOffset,
                                             yOffset, zOffset, width, height, depth,
                                             imageInfo, &uploadWillInitialize))
    {
        return;
    }

    GLenum error = DoCopyTexSubImage(mContext->gl, target, level, xOffset + writeX,
                                     yOffset + writeY, zOffset, readX, readY, rwWidth,
                                     rwHeight);

    if (error == LOCAL_GL_OUT_OF_MEMORY) {
        mContext->ErrorOutOfMemory("%s: Ran out of memory during texture copy.",
                                   funcName);
        return;
    }
    if (error) {
        MOZ_RELEASE_ASSERT(false, "We should have caught all other errors.");
        mContext->GenerateWarning("%s: Unexpected error during texture copy. Context"
                                  " lost.",
                                  funcName);
        mContext->ForceLoseContext();
        return;
    }

    ////////////////////////////////////
    // Update our specification data?

    if (uploadWillInitialize) {
        imageInfo->SetIsDataInitialized(true, this);
    }
}

} // namespace mozilla
