Blob Blame History Raw
/* -*- 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 "gfxPrefs.h"
#include "GLBlitHelper.h"
#include "GLContext.h"
#include "mozilla/gfx/2D.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/HTMLVideoElement.h"
#include "mozilla/dom/ImageBitmap.h"
#include "mozilla/dom/ImageData.h"
#include "mozilla/MathAlgorithms.h"
#include "mozilla/Scoped.h"
#include "mozilla/Unused.h"
#include "ScopedGLHelpers.h"
#include "TexUnpackBlob.h"
#include "WebGLBuffer.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)
 */

static bool ValidateExtents(WebGLContext* webgl, const char* funcName,
                            GLsizei width, GLsizei height, GLsizei depth,
                            GLint border, uint32_t* const out_width,
                            uint32_t* const out_height,
                            uint32_t* const out_depth) {
  // Check border
  if (border != 0) {
    webgl->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."
     */
    webgl->ErrorInvalidValue("%s: `width`/`height`/`depth` must be >= 0.",
                             funcName);
    return false;
  }

  *out_width = width;
  *out_height = height;
  *out_depth = depth;
  return true;
}

////////////////////////////////////////
// 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 ValidateViewType(WebGLContext* webgl, const char* funcName,
                             GLenum unpackType, const TexImageSource& src) {
  if (!src.mView) return true;
  const auto& view = *(src.mView);

  const auto& jsType = view.Type();
  if (!DoesJSTypeMatchUnpackType(unpackType, jsType)) {
    webgl->ErrorInvalidOperation(
        "%s: ArrayBufferView type not compatible with"
        " `type`.",
        funcName);
    return false;
  }

  return true;
}

static bool ValidateUnpackInfo(WebGLContext* webgl, const char* funcName,
                               const webgl::PackingInfo& pi) {
  if (!webgl->mFormatUsage->AreUnpackEnumsValid(pi.format, pi.type)) {
    webgl->ErrorInvalidEnum("%s: Invalid unpack format/type: 0x%04x/0x%04x",
                            funcName, pi.format, pi.type);
    return false;
  }

  return true;
}

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

static UniquePtr<webgl::TexUnpackBytes> FromView(
    WebGLContext* webgl, const char* funcName, TexImageTarget target,
    uint32_t width, uint32_t height, uint32_t depth,
    const dom::ArrayBufferView* view, GLuint viewElemOffset,
    GLuint viewElemLengthOverride) {
  const bool isClientData = true;
  const uint8_t* bytes = nullptr;
  size_t availByteCount = 0;
  if (view) {
    if (!webgl->ValidateArrayBufferView(
            funcName, *view, viewElemOffset, viewElemLengthOverride,
            const_cast<uint8_t**>(&bytes), &availByteCount)) {
      return nullptr;
    }
  }
  return MakeUnique<webgl::TexUnpackBytes>(webgl, target, width, height, depth,
                                           isClientData, bytes, availByteCount);
}

static UniquePtr<webgl::TexUnpackBytes> FromPboOffset(
    WebGLContext* webgl, const char* funcName, TexImageTarget target,
    uint32_t width, uint32_t height, uint32_t depth, WebGLsizeiptr pboOffset,
    const Maybe<GLsizei>& expectedImageSize) {
  if (pboOffset < 0) {
    webgl->ErrorInvalidValue("%s: offset cannot be negative.", funcName);
    return nullptr;
  }

  const auto& buffer =
      webgl->ValidateBufferSelection(funcName, LOCAL_GL_PIXEL_UNPACK_BUFFER);
  if (!buffer) return nullptr;

  size_t availBufferBytes = buffer->ByteLength();
  if (size_t(pboOffset) > availBufferBytes) {
    webgl->ErrorInvalidOperation("%s: Offset is passed end of buffer.",
                                 funcName);
    return nullptr;
  }
  availBufferBytes -= pboOffset;
  if (expectedImageSize.isSome()) {
    if (expectedImageSize.ref() < 0) {
      webgl->ErrorInvalidValue("%s: ImageSize can't be less than 0.", funcName);
      return nullptr;
    }
    if (size_t(expectedImageSize.ref()) != availBufferBytes) {
      webgl->ErrorInvalidOperation(
          "%s: ImageSize doesn't match the required upload byte size.",
          funcName);
      return nullptr;
    }
    availBufferBytes = size_t(expectedImageSize.ref());
  }
  const bool isClientData = false;
  const auto ptr = (const uint8_t*)pboOffset;
  return MakeUnique<webgl::TexUnpackBytes>(webgl, target, width, height, depth,
                                           isClientData, ptr, availBufferBytes);
}

static UniquePtr<webgl::TexUnpackBlob> FromImageBitmap(
    WebGLContext* webgl, const char* funcName, TexImageTarget target,
    uint32_t width, uint32_t height, uint32_t depth,
    const dom::ImageBitmap& imageBitmap, ErrorResult* aRv) {
    if (imageBitmap.IsWriteOnly()) {
        aRv->Throw(NS_ERROR_DOM_SECURITY_ERR);
        return nullptr;
    }

  UniquePtr<dom::ImageBitmapCloneData> cloneData =
      Move(imageBitmap.ToCloneData());
  const RefPtr<gfx::DataSourceSurface> surf = cloneData->mSurface;

  if (!width) {
    width = surf->GetSize().width;
  }

  if (!height) {
    height = surf->GetSize().height;
  }

  // WhatWG "HTML Living Standard" (30 October 2015):
  // "The getImageData(sx, sy, sw, sh) method [...] Pixels must be returned as
  // non-premultiplied alpha values."
  return MakeUnique<webgl::TexUnpackSurface>(
      webgl, target, width, height, depth, surf, cloneData->mAlphaType);
}

static UniquePtr<webgl::TexUnpackBlob> FromImageData(
    WebGLContext* webgl, const char* funcName, TexImageTarget target,
    uint32_t width, uint32_t height, uint32_t depth,
    const dom::ImageData& imageData, dom::Uint8ClampedArray* scopedArr) {
  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;

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

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

  uint8_t* wrappableData = (uint8_t*)data;

  const RefPtr<gfx::DataSourceSurface> surf =
      gfx::Factory::CreateWrappingDataSourceSurface(wrappableData, stride, size,
                                                    surfFormat);
  if (!surf) {
    webgl->ErrorOutOfMemory("%s: OOM in FromImageData.", funcName);
    return nullptr;
  }

  ////

  if (!width) {
    width = imageData.Width();
  }

  if (!height) {
    height = imageData.Height();
  }

  ////

  return MakeUnique<webgl::TexUnpackSurface>(webgl, target, width, height,
                                             depth, surf, alphaType);
}

UniquePtr<webgl::TexUnpackBlob> WebGLContext::FromDomElem(
    const char* funcName, TexImageTarget target, uint32_t width,
    uint32_t height, uint32_t depth, const dom::Element& elem,
    ErrorResult* const out_error) {
    if (elem.IsHTMLElement(nsGkAtoms::canvas)) {
        const dom::HTMLCanvasElement* canvas = static_cast<const dom::HTMLCanvasElement*>(&elem);
        if (canvas->IsWriteOnly()) {
            out_error->Throw(NS_ERROR_DOM_SECURITY_ERR);
            return nullptr;
        }
    }

  // The canvas spec says that drawImage should draw the first frame of
  // animated images. The webgl spec doesn't mention the issue, so we do the
  // same as drawImage.
  uint32_t flags = nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE |
                   nsLayoutUtils::SFE_WANT_IMAGE_SURFACE |
                   nsLayoutUtils::SFE_USE_ELEMENT_SIZE_IF_VECTOR;

  if (mPixelStore_ColorspaceConversion == LOCAL_GL_NONE)
    flags |= nsLayoutUtils::SFE_NO_COLORSPACE_CONVERSION;

  if (!mPixelStore_PremultiplyAlpha)
    flags |= nsLayoutUtils::SFE_PREFER_NO_PREMULTIPLY_ALPHA;

  RefPtr<gfx::DrawTarget> idealDrawTarget = nullptr;  // Don't care for now.
  auto sfer = nsLayoutUtils::SurfaceFromElement(
      const_cast<dom::Element*>(&elem), flags, idealDrawTarget);

  //////

  uint32_t elemWidth = 0;
  uint32_t elemHeight = 0;
  layers::Image* layersImage = nullptr;
  if (!gfxPrefs::WebGLDisableDOMBlitUploads() && sfer.mLayersImage) {
    layersImage = sfer.mLayersImage;
    elemWidth = layersImage->GetSize().width;
    elemHeight = layersImage->GetSize().height;
  }

  RefPtr<gfx::DataSourceSurface> dataSurf;
  if (!layersImage && sfer.GetSourceSurface()) {
    const auto surf = sfer.GetSourceSurface();
    elemWidth = surf->GetSize().width;
    elemHeight = surf->GetSize().height;

    // WARNING: OSX can lose our MakeCurrent here.
    dataSurf = surf->GetDataSurface();
  }

  //////

  if (!width) {
    width = elemWidth;
  }

  if (!height) {
    height = elemHeight;
  }

  ////

  if (!layersImage && !dataSurf) {
    const bool isClientData = true;
    return MakeUnique<webgl::TexUnpackBytes>(this, target, width, height, depth,
                                             isClientData, nullptr, 0);
  }

  //////

  // 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..

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

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

  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.
    GenerateWarning("%s: Element is write-only, thus cannot be uploaded.",
                    funcName);
    out_error->Throw(NS_ERROR_DOM_SECURITY_ERR);
    return nullptr;
  }

  //////
  // Ok, we're good!

  if (layersImage) {
    return MakeUnique<webgl::TexUnpackImage>(this, target, width, height, depth,
                                             layersImage, sfer.mAlphaType);
  }

  MOZ_ASSERT(dataSurf);
  return MakeUnique<webgl::TexUnpackSurface>(this, target, width, height, depth,
                                             dataSurf, sfer.mAlphaType);
}

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

UniquePtr<webgl::TexUnpackBlob> WebGLContext::From(
    const char* funcName, TexImageTarget target, GLsizei rawWidth,
    GLsizei rawHeight, GLsizei rawDepth, GLint border,
    const TexImageSource& src, dom::Uint8ClampedArray* const scopedArr) {
  uint32_t width, height, depth;
  if (!ValidateExtents(this, funcName, rawWidth, rawHeight, rawDepth, border,
                       &width, &height, &depth)) {
    return nullptr;
  }

  if (src.mPboOffset) {
    return FromPboOffset(this, funcName, target, width, height, depth,
                         *(src.mPboOffset), Nothing());
  }

  if (mBoundPixelUnpackBuffer) {
    ErrorInvalidOperation("%s: PIXEL_UNPACK_BUFFER must be null.", funcName);
    return nullptr;
  }

  if (src.mImageBitmap) {
    return FromImageBitmap(this, funcName, target, width, height, depth,
                           *(src.mImageBitmap), src.mOut_error);
  }

  if (src.mImageData) {
    return FromImageData(this, funcName, target, width, height, depth,
                         *(src.mImageData), scopedArr);
  }

  if (src.mDomElem) {
    return FromDomElem(funcName, target, width, height, depth, *(src.mDomElem),
                       src.mOut_error);
  }

  return FromView(this, funcName, target, width, height, depth, src.mView,
                  src.mViewElemOffset, src.mViewElemLengthOverride);
}

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

static UniquePtr<webgl::TexUnpackBlob> ValidateTexOrSubImage(
    WebGLContext* webgl, const char* funcName, TexImageTarget target,
    GLsizei rawWidth, GLsizei rawHeight, GLsizei rawDepth, GLint border,
    const webgl::PackingInfo& pi, const TexImageSource& src,
    dom::Uint8ClampedArray* const scopedArr) {
  if (!ValidateUnpackInfo(webgl, funcName, pi)) return nullptr;

  if (!ValidateViewType(webgl, funcName, pi.type, src)) return nullptr;

  auto blob = webgl->From(funcName, target, rawWidth, rawHeight, rawDepth,
                          border, src, scopedArr);
  if (!blob || !blob->Validate(webgl, funcName, pi)) return nullptr;

  return Move(blob);
}

void WebGLTexture::TexImage(const char* funcName, TexImageTarget target,
                            GLint level, GLenum internalFormat, GLsizei width,
                            GLsizei height, GLsizei depth, GLint border,
                            const webgl::PackingInfo& pi,
                            const TexImageSource& src) {
  dom::RootedSpiderMonkeyInterface<dom::Uint8ClampedArray> scopedArr(
      dom::RootingCx());
  const auto blob =
      ValidateTexOrSubImage(mContext, funcName, target, width, height, depth,
                            border, pi, src, &scopedArr);
  if (!blob) return;

  TexImage(funcName, target, level, internalFormat, pi, blob.get());
}

void WebGLTexture::TexSubImage(const char* funcName, TexImageTarget target,
                               GLint level, GLint xOffset, GLint yOffset,
                               GLint zOffset, GLsizei width, GLsizei height,
                               GLsizei depth, const webgl::PackingInfo& pi,
                               const TexImageSource& src) {
  const GLint border = 0;
  dom::RootedSpiderMonkeyInterface<dom::Uint8ClampedArray> scopedArr(
      dom::RootingCx());
  const auto blob =
      ValidateTexOrSubImage(mContext, funcName, target, width, height, depth,
                            border, pi, src, &scopedArr);
  if (!blob) return;

  if (!blob->HasData()) {
    mContext->ErrorInvalidValue("%s: Source must not be null.", funcName);
    return;
  }

  TexSubImage(funcName, target, level, xOffset, yOffset, zOffset, pi,
              blob.get());
}

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

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 rawLevel, uint32_t width,
    uint32_t height, uint32_t depth,
    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, rawLevel, &imageInfo))
    return false;
  const uint32_t level(rawLevel);

  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 mGLMaxTextureSize, which is neither
   * necessarily lower nor higher than MAX_TEXTURE_SIZE.
   *
   * Note that mGLMaxTextureSize must be >= than the advertized
   * MAX_TEXTURE_SIZE. For simplicity, we advertize MAX_TEXTURE_SIZE as
   * mGLMaxTextureSize.
   */

  uint32_t maxWidthHeight = 0;
  uint32_t maxDepth = 0;
  uint32_t maxLevel = 0;

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

    case LOCAL_GL_TEXTURE_3D:
      maxWidthHeight = mContext->mGLMax3DTextureSize >> level;
      maxDepth = maxWidthHeight;
      maxLevel = CeilingLog2(mContext->mGLMax3DTextureSize);
      break;

    case LOCAL_GL_TEXTURE_2D_ARRAY:
      maxWidthHeight = mContext->mGLMaxTextureSize >> 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->mGLMaxArrayTextureLayers;
      maxLevel = CeilingLog2(mContext->mGLMaxTextureSize);
      break;

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

  if (level > maxLevel) {
    mContext->ErrorInvalidValue(
        "%s: Requested level is not supported for target.", funcName);
    return false;
  }

  if (width > maxWidthHeight || height > maxWidthHeight || 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(width) || !IsPowerOfTwo(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, uint32_t width, uint32_t height,
    uint32_t depth, WebGLTexture::ImageInfo** const out_imageInfo) {
  // The conformance test wants bad arg checks before imageInfo checks.
  if (xOffset < 0 || yOffset < 0 || zOffset < 0) {
    mContext->ErrorInvalidValue("%s: Offsets 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 %zu)",
        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("GFX: bad target");
  }

  return errorScope.GetError();
}

bool IsTarget3D(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("GFX: 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 (IsTarget3D(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 (IsTarget3D(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,
                                          GLsizei dataSize, const void* data) {
  const GLint border = 0;

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

  if (IsTarget3D(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 (IsTarget3D(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 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 (IsTarget3D(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::ASTC:
      if (target == LOCAL_GL_TEXTURE_3D &&
          !webgl->gl->IsExtensionSupported(
              gl::GLContext::KHR_texture_compression_astc_hdr)) {
        webgl->ErrorInvalidOperation(
            "%s: TEXTURE_3D requires ASTC's hdr profile.", funcName);
        return false;
      }
      break;

    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;

  WebGLTexture::ImageInfo* testImageInfo;
  if (!ValidateTexImageSpecification(funcName, testTarget, testLevel, width,
                                     height, depth, &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!

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

  mContext->OnDataAllocCall();

  if (error == LOCAL_GL_OUT_OF_MEMORY) {
    mContext->ErrorOutOfMemory(
        "%s: Ran out of memory during texture allocation.", funcName);
    Truncate();
    return;
  }
  if (error) {
    MOZ_RELEASE_ASSERT(false, "GFX: 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(funcName, 0, newInfo);

  PopulateMipChain(funcName, 0, levels - 1);

  mImmutable = true;
  mImmutableLevelCount = levels;
}

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

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

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

  const auto& fua = mContext->mFormatUsage;
  if (!fua->IsInternalFormatEnumValid(internalFormat)) {
    mContext->ErrorInvalidValue("%s: Invalid internalformat: 0x%04x", funcName,
                                internalFormat);
    return;
  }

  auto dstUsage = fua->GetSizedTexUsage(internalFormat);
  if (!dstUsage) {
    if (internalFormat != pi.format) {
      /* GL ES Version 3.0.4 - 3.8.3 Texture Image Specification
       *   "Specifying a combination of values for format, type, and
       *   internalformat that is not listed as a valid combination
       *   in tables 3.2 or 3.3 generates the error INVALID_OPERATION."
       */
      mContext->ErrorInvalidOperation(
          "%s: Unsized internalFormat must match"
          " unpack format.",
          funcName);
      return;
    }

    dstUsage = fua->GetUnsizedTexUsage(pi);
  }

  if (!dstUsage) {
    mContext->ErrorInvalidOperation(
        "%s: Invalid internalformat/format/type:"
        " 0x%04x/0x%04x/0x%04x",
        funcName, internalFormat, pi.format, pi.type);
    return;
  }

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

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

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

  if (!mContext->IsWebGL2() && dstFormat->d) {
    if (target != LOCAL_GL_TEXTURE_2D || blob->HasData() || 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!

  // 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->HasData());

  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;
  if (!blob->TexOrSubImage(isSubImage, needsRespec, funcName, this, target,
                           level, driverUnpackInfo, xOffset, yOffset, zOffset,
                           pi, &glError)) {
    return;
  }

  mContext->OnDataAllocCall();

  if (glError == LOCAL_GL_OUT_OF_MEMORY) {
    mContext->ErrorOutOfMemory("%s: Driver ran out of memory during upload.",
                               funcName);
    Truncate();
    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(funcName, imageInfo, newImageInfo);
}

void WebGLTexture::TexSubImage(const char* funcName, TexImageTarget target,
                               GLint level, GLint xOffset, GLint yOffset,
                               GLint zOffset, const webgl::PackingInfo& pi,
                               const 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->d) {
    mContext->ErrorInvalidOperation(
        "%s: Function may not be called on a texture of"
        " format %s.",
        funcName, dstFormat->name);
    return;
  }

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

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

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

  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;
  if (!blob->TexOrSubImage(isSubImage, needsRespec, funcName, this, target,
                           level, driverUnpackInfo, xOffset, yOffset, zOffset,
                           pi, &glError)) {
    return;
  }

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

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

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

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

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

UniquePtr<webgl::TexUnpackBytes> WebGLContext::FromCompressed(
    const char* funcName, TexImageTarget target, GLsizei rawWidth,
    GLsizei rawHeight, GLsizei rawDepth, GLint border,
    const TexImageSource& src, const Maybe<GLsizei>& expectedImageSize) {
  uint32_t width, height, depth;
  if (!ValidateExtents(this, funcName, rawWidth, rawHeight, rawDepth, border,
                       &width, &height, &depth)) {
    return nullptr;
  }

  if (src.mPboOffset) {
    return FromPboOffset(this, funcName, target, width, height, depth,
                         *(src.mPboOffset), expectedImageSize);
  }

  if (mBoundPixelUnpackBuffer) {
    ErrorInvalidOperation("%s: PIXEL_UNPACK_BUFFER must be null.", funcName);
    return nullptr;
  }

  return FromView(this, funcName, target, width, height, depth, src.mView,
                  src.mViewElemOffset, src.mViewElemLengthOverride);
}

void WebGLTexture::CompressedTexImage(const char* funcName,
                                      TexImageTarget target, GLint level,
                                      GLenum internalFormat, GLsizei rawWidth,
                                      GLsizei rawHeight, GLsizei rawDepth,
                                      GLint border, const TexImageSource& src,
                                      const Maybe<GLsizei>& expectedImageSize) {
  const auto blob =
      mContext->FromCompressed(funcName, target, rawWidth, rawHeight, rawDepth,
                               border, src, expectedImageSize);
  if (!blob) return;

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

  WebGLTexture::ImageInfo* imageInfo;
  if (!ValidateTexImageSpecification(funcName, target, level, blob->mWidth,
                                     blob->mHeight, blob->mDepth, &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

  if (!ValidateCompressedTexUnpack(mContext, funcName, blob->mWidth,
                                   blob->mHeight, blob->mDepth, format,
                                   blob->mAvailBytes)) {
    return;
  }

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

  if (!ValidateCompressedTexImageRestrictions(funcName, mContext, target, level,
                                              format, blob->mWidth,
                                              blob->mHeight, blob->mDepth)) {
    return;
  }

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

  const ScopedLazyBind bindPBO(mContext->gl, LOCAL_GL_PIXEL_UNPACK_BUFFER,
                               mContext->mBoundPixelUnpackBuffer);

  // Warning: Possibly shared memory.  See bug 1225033.
  GLenum error = DoCompressedTexImage(
      mContext->gl, target, level, internalFormat, blob->mWidth, blob->mHeight,
      blob->mDepth, blob->mAvailBytes, blob->mPtr);
  mContext->OnDataAllocCall();
  if (error == LOCAL_GL_OUT_OF_MEMORY) {
    mContext->ErrorOutOfMemory("%s: Ran out of memory during upload.",
                               funcName);
    Truncate();
    return;
  }
  if (error) {
    MOZ_RELEASE_ASSERT(false, "GFX: 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, blob->mWidth, blob->mHeight, blob->mDepth,
                               isDataInitialized);
  SetImageInfo(funcName, 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 rawWidth, GLsizei rawHeight,
    GLsizei rawDepth, GLenum sizedUnpackFormat, const TexImageSource& src,
    const Maybe<GLsizei>& expectedImageSize) {
  const GLint border = 0;
  const auto blob =
      mContext->FromCompressed(funcName, target, rawWidth, rawHeight, rawDepth,
                               border, src, expectedImageSize);
  if (!blob) return;

  ////////////////////////////////////
  // 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;

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

  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, blob->mWidth,
                                   blob->mHeight, blob->mDepth, format,
                                   blob->mAvailBytes)) {
    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, blob->mWidth, blob->mHeight)) {
        mContext->ErrorInvalidOperation(
            "%s: Format requires block-aligned sub-image"
            " updates.",
            funcName);
        return;
      }
      break;

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

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

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

  const ScopedLazyBind bindPBO(mContext->gl, LOCAL_GL_PIXEL_UNPACK_BUFFER,
                               mContext->mBoundPixelUnpackBuffer);

  // Warning: Possibly shared memory.  See bug 1225033.
  GLenum error = DoCompressedTexSubImage(
      mContext->gl, target, level, xOffset, yOffset, zOffset, blob->mWidth,
      blob->mHeight, blob->mDepth, sizedUnpackFormat, blob->mAvailBytes,
      blob->mPtr);
  if (error == LOCAL_GL_OUT_OF_MEMORY) {
    mContext->ErrorOutOfMemory("%s: Ran out of memory during upload.",
                               funcName);
    Truncate();
    return;
  }
  if (error) {
    MOZ_RELEASE_ASSERT(false, "GFX: 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("GFX: Should be able to request CopyTexImage from Float.");

    default:
      MOZ_CRASH("GFX: 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("GFX: 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("GFX: 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);
  {
    const gl::ScopedBindFramebuffer bindFB(gl, rgbaFB);
    gl->BlitHelper()->DrawBlitTextureToFramebuffer(scopedTex.Texture(), 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);
}

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

static bool GetUnsizedFormatForCopy(GLenum internalFormat,
                                    webgl::UnsizedFormat* const out) {
  switch (internalFormat) {
    case LOCAL_GL_RED:
      *out = webgl::UnsizedFormat::R;
      break;
    case LOCAL_GL_RG:
      *out = webgl::UnsizedFormat::RG;
      break;
    case LOCAL_GL_RGB:
      *out = webgl::UnsizedFormat::RGB;
      break;
    case LOCAL_GL_RGBA:
      *out = webgl::UnsizedFormat::RGBA;
      break;
    case LOCAL_GL_LUMINANCE:
      *out = webgl::UnsizedFormat::L;
      break;
    case LOCAL_GL_ALPHA:
      *out = webgl::UnsizedFormat::A;
      break;
    case LOCAL_GL_LUMINANCE_ALPHA:
      *out = webgl::UnsizedFormat::LA;
      break;

    default:
      return false;
  }

  return true;
}

static const webgl::FormatUsageInfo* ValidateCopyDestUsage(
    const char* funcName, WebGLContext* webgl,
    const webgl::FormatInfo* srcFormat, GLenum internalFormat) {
  const auto& fua = webgl->mFormatUsage;

  auto dstUsage = fua->GetSizedTexUsage(internalFormat);
  if (!dstUsage) {
    // Ok, maybe it's unsized.
    webgl::UnsizedFormat unsizedFormat;
    if (!GetUnsizedFormatForCopy(internalFormat, &unsizedFormat)) {
      webgl->ErrorInvalidEnum("%s: Unrecongnized internalFormat 0x%04x.",
                              funcName, internalFormat);
      return nullptr;
    }

    const auto dstFormat = srcFormat->GetCopyDecayFormat(unsizedFormat);
    if (dstFormat) {
      dstUsage = fua->GetUsage(dstFormat->effectiveFormat);
    }
    if (!dstUsage) {
      webgl->ErrorInvalidOperation(
          "%s: 0x%04x is not a valid unsized format for"
          " source format %s.",
          funcName, internalFormat, srcFormat->name);
      return nullptr;
    }

    return dstUsage;
  }
  // Alright, it's sized.

  const auto dstFormat = dstUsage->format;

  const auto fnNarrowType = [&](webgl::ComponentType type) {
    switch (type) {
      case webgl::ComponentType::NormInt:
      case webgl::ComponentType::NormUInt:
        // These both count as "fixed-point".
        return webgl::ComponentType::NormInt;

      default:
        return type;
    }
  };

  const auto srcType = fnNarrowType(srcFormat->componentType);
  const auto dstType = fnNarrowType(dstFormat->componentType);
  if (dstType != srcType) {
    webgl->ErrorInvalidOperation(
        "%s: For sized internalFormats, source and dest"
        " component types must match. (source: %s, dest:"
        " %s)",
        funcName, srcFormat->name, dstFormat->name);
    return nullptr;
  }

  bool componentSizesMatch = true;
  if (dstFormat->r) {
    componentSizesMatch &= (dstFormat->r == srcFormat->r);
  }
  if (dstFormat->g) {
    componentSizesMatch &= (dstFormat->g == srcFormat->g);
  }
  if (dstFormat->b) {
    componentSizesMatch &= (dstFormat->b == srcFormat->b);
  }
  if (dstFormat->a) {
    componentSizesMatch &= (dstFormat->a == srcFormat->a);
  }

  if (!componentSizesMatch) {
    webgl->ErrorInvalidOperation(
        "%s: For sized internalFormats, source and dest"
        " component sizes must match exactly. (source: %s,"
        " dest: %s)",
        funcName, srcFormat->name, dstFormat->name);
    return nullptr;
  }

  return dstUsage;
}

bool WebGLTexture::ValidateCopyTexImageForFeedback(const char* funcName,
                                                   uint32_t level,
                                                   GLint layer) const {
  const auto& fb = mContext->mBoundReadFramebuffer;
  if (fb) {
    const auto& attach = fb->ColorReadBuffer();
    MOZ_ASSERT(attach);

    if (attach->Texture() == this && attach->Layer() == layer &&
        uint32_t(attach->MipLevel()) == level) {
      // Note that the TexImageTargets *don't* have to match for this to be
      // undefined per GLES 3.0.4 p211, thus an INVALID_OP in WebGL.
      mContext->ErrorInvalidOperation(
          "%s: Feedback loop detected, as this texture"
          " is already attached to READ_FRAMEBUFFER's"
          " READ_BUFFER-selected COLOR_ATTACHMENT%u.",
          funcName, attach->mAttachmentPoint);
      return false;
    }
  }
  return true;
}

static bool DoCopyTexOrSubImage(WebGLContext* webgl, const char* funcName,
                                bool isSubImage, WebGLTexture* tex,
                                TexImageTarget target, GLint level,
                                GLint xWithinSrc, GLint yWithinSrc,
                                uint32_t srcTotalWidth, uint32_t srcTotalHeight,
                                const webgl::FormatUsageInfo* srcUsage,
                                GLint xOffset, GLint yOffset, GLint zOffset,
                                uint32_t dstWidth, uint32_t dstHeight,
                                const webgl::FormatUsageInfo* dstUsage) {
  const auto& gl = webgl->gl;

  ////

  int32_t readX, readY;
  int32_t writeX, writeY;
  int32_t rwWidth, rwHeight;
  if (!Intersect(srcTotalWidth, xWithinSrc, dstWidth, &readX, &writeX,
                 &rwWidth) ||
      !Intersect(srcTotalHeight, yWithinSrc, dstHeight, &readY, &writeY,
                 &rwHeight)) {
    webgl->ErrorOutOfMemory("%s: Bad subrect selection.", funcName);
    return false;
  }

  writeX += xOffset;
  writeY += yOffset;

  ////

  GLenum error = 0;
  do {
    const auto& idealUnpack = dstUsage->idealUnpack;
    if (!isSubImage) {
      UniqueBuffer buffer;

      if (uint32_t(rwWidth) != dstWidth || uint32_t(rwHeight) != dstHeight) {
        const auto& pi = idealUnpack->ToPacking();
        CheckedUint32 byteCount = BytesPerPixel(pi);
        byteCount *= dstWidth;
        byteCount *= dstHeight;

        if (byteCount.isValid()) {
          buffer = calloc(1, byteCount.value());
        }

        if (!buffer.get()) {
          webgl->ErrorOutOfMemory("%s: Ran out of memory allocating zeros.",
                                  funcName);
          return false;
        }
      }

      const ScopedUnpackReset unpackReset(webgl);
      gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 1);
      error = DoTexImage(gl, target, level, idealUnpack, dstWidth, dstHeight, 1,
                         buffer.get());
      if (error) break;
    }

    if (!rwWidth || !rwHeight) {
      // There aren't any pixels to copy, so we're 'done'.
      return true;
    }

    const auto& srcFormat = srcUsage->format;
    ScopedCopyTexImageSource maybeSwizzle(webgl, funcName, srcTotalWidth,
                                          srcTotalHeight, srcFormat, dstUsage);

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

    return true;
  } while (false);

  if (error == LOCAL_GL_OUT_OF_MEMORY) {
    webgl->ErrorOutOfMemory("%s: Ran out of memory during texture copy.",
                            funcName);
    tex->Truncate();
    return false;
  }

  if (gl->IsANGLE() && error == LOCAL_GL_INVALID_OPERATION) {
    webgl->ErrorImplementationBug(
        "%s: ANGLE is particular about CopyTexSubImage"
        " formats matching exactly.",
        funcName);
    return false;
  }

  MOZ_RELEASE_ASSERT(false, "GFX: We should have caught all other errors.");
  webgl->GenerateWarning(
      "%s: Unexpected error during texture copy. Context lost.", funcName);
  webgl->ForceLoseContext();
  return false;
}

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

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

  uint32_t width, height, depth;
  if (!ValidateExtents(mContext, funcName, rawWidth, rawHeight, 1, border,
                       &width, &height, &depth)) {
    return;
  }

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

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

  const webgl::FormatUsageInfo* srcUsage;
  uint32_t srcTotalWidth;
  uint32_t srcTotalHeight;
  if (!mContext->BindCurFBForColorRead(funcName, &srcUsage, &srcTotalWidth,
                                       &srcTotalHeight)) {
    return;
  }

  if (!ValidateCopyTexImageForFeedback(funcName, level)) return;

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

  const auto& srcFormat = srcUsage->format;
  const auto dstUsage =
      ValidateCopyDestUsage(funcName, mContext, srcFormat, internalFormat);
  if (!dstUsage) return;

  const auto& dstFormat = dstUsage->format;
  if (!ValidateTargetForFormat(funcName, mContext, target, dstFormat)) return;

  if (!mContext->IsWebGL2() && dstFormat->d) {
    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!

  const bool isSubImage = false;
  if (!DoCopyTexOrSubImage(mContext, funcName, isSubImage, this, target, level,
                           x, y, srcTotalWidth, srcTotalHeight, srcUsage, 0, 0,
                           0, width, height, dstUsage)) {
    return;
  }

  mContext->OnDataAllocCall();

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

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

void WebGLTexture::CopyTexSubImage(const char* funcName, TexImageTarget target,
                                   GLint level, GLint xOffset, GLint yOffset,
                                   GLint zOffset, GLint x, GLint y,
                                   GLsizei rawWidth, GLsizei rawHeight) {
  uint32_t width, height, depth;
  if (!ValidateExtents(mContext, funcName, rawWidth, rawHeight, 1, 0, &width,
                       &height, &depth)) {
    return;
  }

  ////////////////////////////////////
  // 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->d) {
    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 srcTotalWidth;
  uint32_t srcTotalHeight;
  if (!mContext->BindCurFBForColorRead(funcName, &srcUsage, &srcTotalWidth,
                                       &srcTotalHeight)) {
    return;
  }

  if (!ValidateCopyTexImageForFeedback(funcName, level, zOffset)) return;

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

  auto srcFormat = srcUsage->format;
  if (!ValidateCopyTexImageFormats(mContext, funcName, srcFormat, dstFormat))
    return;

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

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

  const bool isSubImage = true;
  if (!DoCopyTexOrSubImage(mContext, funcName, isSubImage, this, target, level,
                           x, y, srcTotalWidth, srcTotalHeight, srcUsage,
                           xOffset, yOffset, zOffset, width, height,
                           dstUsage)) {
    return;
  }

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

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

}  // namespace mozilla