diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /dom/canvas/WebGLTextureUpload.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/canvas/WebGLTextureUpload.cpp')
-rw-r--r-- | dom/canvas/WebGLTextureUpload.cpp | 1921 |
1 files changed, 1921 insertions, 0 deletions
diff --git a/dom/canvas/WebGLTextureUpload.cpp b/dom/canvas/WebGLTextureUpload.cpp new file mode 100644 index 0000000000..2837c0b602 --- /dev/null +++ b/dom/canvas/WebGLTextureUpload.cpp @@ -0,0 +1,1921 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* 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 <limits> + +#include "CanvasUtils.h" +#include "ClientWebGLContext.h" +#include "GLBlitHelper.h" +#include "GLContext.h" +#include "mozilla/Casting.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/gfx/Logging.h" +#include "mozilla/dom/HTMLCanvasElement.h" +#include "mozilla/dom/HTMLVideoElement.h" +#include "mozilla/dom/ImageBitmap.h" +#include "mozilla/dom/ImageData.h" +#include "mozilla/dom/OffscreenCanvas.h" +#include "mozilla/MathAlgorithms.h" +#include "mozilla/Scoped.h" +#include "mozilla/ScopeExit.h" +#include "mozilla/StaticPrefs_webgl.h" +#include "mozilla/Unused.h" +#include "nsLayoutUtils.h" +#include "ScopedGLHelpers.h" +#include "TexUnpackBlob.h" +#include "WebGLBuffer.h" +#include "WebGLContext.h" +#include "WebGLContextUtils.h" +#include "WebGLFramebuffer.h" +#include "WebGLTexelConversions.h" + +namespace mozilla { +namespace webgl { + +Maybe<TexUnpackBlobDesc> FromImageBitmap(const GLenum target, Maybe<uvec3> size, + const dom::ImageBitmap& imageBitmap, + ErrorResult* const out_rv) { + if (imageBitmap.IsWriteOnly()) { + out_rv->Throw(NS_ERROR_DOM_SECURITY_ERR); + return {}; + } + + const auto cloneData = imageBitmap.ToCloneData(); + if (!cloneData) { + return {}; + } + + const RefPtr<gfx::DataSourceSurface> surf = cloneData->mSurface; + const auto imageSize = *uvec2::FromSize(surf->GetSize()); + if (!size) { + size.emplace(imageSize.x, imageSize.y, 1); + } + + // WhatWG "HTML Living Standard" (30 October 2015): + // "The getImageData(sx, sy, sw, sh) method [...] Pixels must be returned as + // non-premultiplied alpha values." + return Some(TexUnpackBlobDesc{target, + size.value(), + cloneData->mAlphaType, + {}, + {}, + Some(imageSize), + nullptr, + {}, + surf, + {}, + false}); +} + +static layers::SurfaceDescriptor Flatten(const layers::SurfaceDescriptor& sd) { + const auto sdType = sd.type(); + if (sdType != layers::SurfaceDescriptor::TSurfaceDescriptorGPUVideo) { + return sd; + } + const auto& sdv = sd.get_SurfaceDescriptorGPUVideo(); + const auto& sdvType = sdv.type(); + if (sdvType != + layers::SurfaceDescriptorGPUVideo::TSurfaceDescriptorRemoteDecoder) { + return sd; + } + + const auto& sdrd = sdv.get_SurfaceDescriptorRemoteDecoder(); + const auto& subdesc = sdrd.subdesc(); + const auto& subdescType = subdesc.type(); + switch (subdescType) { + case layers::RemoteDecoderVideoSubDescriptor::T__None: + case layers::RemoteDecoderVideoSubDescriptor::Tnull_t: + return sd; + + case layers::RemoteDecoderVideoSubDescriptor::TSurfaceDescriptorD3D10: + return subdesc.get_SurfaceDescriptorD3D10(); + case layers::RemoteDecoderVideoSubDescriptor::TSurfaceDescriptorDXGIYCbCr: + return subdesc.get_SurfaceDescriptorDXGIYCbCr(); + case layers::RemoteDecoderVideoSubDescriptor::TSurfaceDescriptorDMABuf: + return subdesc.get_SurfaceDescriptorDMABuf(); + case layers::RemoteDecoderVideoSubDescriptor:: + TSurfaceDescriptorMacIOSurface: + return subdesc.get_SurfaceDescriptorMacIOSurface(); + case layers::RemoteDecoderVideoSubDescriptor:: + TSurfaceDescriptorDcompSurface: + return subdesc.get_SurfaceDescriptorDcompSurface(); + } + MOZ_CRASH("unreachable"); +} + +Maybe<webgl::TexUnpackBlobDesc> FromOffscreenCanvas( + const ClientWebGLContext& webgl, const GLenum target, Maybe<uvec3> size, + const dom::OffscreenCanvas& canvas, ErrorResult* const out_error) { + if (canvas.IsWriteOnly()) { + webgl.EnqueueWarning( + "OffscreenCanvas is write-only, thus cannot be uploaded."); + out_error->ThrowSecurityError( + "OffscreenCanvas is write-only, thus cannot be uploaded."); + return {}; + } + + // 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; + auto sfer = nsLayoutUtils::SurfaceFromOffscreenCanvas( + const_cast<dom::OffscreenCanvas*>(&canvas), flags); + + RefPtr<gfx::DataSourceSurface> dataSurf; + if (sfer.GetSourceSurface()) { + dataSurf = sfer.GetSourceSurface()->GetDataSurface(); + } + + if (!dataSurf) { + webgl.EnqueueWarning("Resource has no data (yet?). Uploading zeros."); + if (!size) { + size.emplace(0, 0, 1); + } + return Some( + TexUnpackBlobDesc{target, size.value(), gfxAlphaType::NonPremult}); + } + + // We checked this above before we requested the surface. + MOZ_RELEASE_ASSERT(!sfer.mIsWriteOnly); + + uvec2 canvasSize = *uvec2::FromSize(dataSurf->GetSize()); + if (!size) { + size.emplace(canvasSize.x, canvasSize.y, 1); + } + + return Some(TexUnpackBlobDesc{target, + size.value(), + sfer.mAlphaType, + {}, + {}, + Some(canvasSize), + {}, + {}, + dataSurf}); +} + +Maybe<webgl::TexUnpackBlobDesc> FromDomElem(const ClientWebGLContext& webgl, + const GLenum target, + Maybe<uvec3> size, + const dom::Element& elem, + ErrorResult* const out_error) { + if (elem.IsHTMLElement(nsGkAtoms::canvas)) { + const dom::HTMLCanvasElement* srcCanvas = + static_cast<const dom::HTMLCanvasElement*>(&elem); + if (srcCanvas->IsWriteOnly()) { + out_error->Throw(NS_ERROR_DOM_SECURITY_ERR); + return {}; + } + } + + // 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_USE_ELEMENT_SIZE_IF_VECTOR | + nsLayoutUtils::SFE_EXACT_SIZE_SURFACE | + nsLayoutUtils::SFE_ALLOW_NON_PREMULT; + const auto& unpacking = webgl.State().mPixelUnpackState; + if (unpacking.colorspaceConversion == LOCAL_GL_NONE) { + flags |= nsLayoutUtils::SFE_NO_COLORSPACE_CONVERSION; + } + + RefPtr<gfx::DrawTarget> idealDrawTarget = nullptr; // Don't care for now. + auto sfer = nsLayoutUtils::SurfaceFromElement( + const_cast<dom::Element*>(&elem), flags, idealDrawTarget); + + ////// + + uvec2 elemSize; + + const auto& layersImage = sfer.mLayersImage; + Maybe<layers::SurfaceDescriptor> sd; + if (layersImage) { + elemSize = *uvec2::FromSize(layersImage->GetSize()); + + sd = layersImage->GetDesc(); + if (sd) { + sd = Some(Flatten(*sd)); + } + if (!sd) { + NS_WARNING("No SurfaceDescriptor for layers::Image!"); + } + } + + RefPtr<gfx::DataSourceSurface> dataSurf; + if (!sd && sfer.GetSourceSurface()) { + const auto surf = sfer.GetSourceSurface(); + elemSize = *uvec2::FromSize(surf->GetSize()); + + // WARNING: OSX can lose our MakeCurrent here. + dataSurf = surf->GetDataSurface(); + } + + ////// + + if (!size) { + size.emplace(elemSize.x, elemSize.y, 1); + } + + //// + + if (!sd && !dataSurf) { + webgl.EnqueueWarning("Resource has no data (yet?). Uploading zeros."); + if (!size) { + size.emplace(0, 0, 1); + } + return Some( + TexUnpackBlobDesc{target, size.value(), gfxAlphaType::NonPremult}); + } + + ////// + + // 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 = webgl.PrincipalOrNull(); + if (!dstPrincipal || !dstPrincipal->Subsumes(srcPrincipal)) { + webgl.EnqueueWarning("Cross-origin elements require CORS."); + 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. + webgl.EnqueueWarning("Element is write-only, thus cannot be uploaded."); + out_error->Throw(NS_ERROR_DOM_SECURITY_ERR); + return {}; + } + + ////// + // Ok, we're good! + + return Some(TexUnpackBlobDesc{target, + size.value(), + sfer.mAlphaType, + {}, + {}, + Some(elemSize), + layersImage, + sd, + dataSurf}); +} + +} // namespace webgl + +////////////////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////////////////// + +static bool ValidateTexImage(WebGLContext* webgl, WebGLTexture* texture, + TexImageTarget target, uint32_t level, + webgl::ImageInfo** const out_imageInfo) { + // Check level + if (level >= WebGLTexture::kMaxLevelCount) { + webgl->ErrorInvalidValue("`level` is too large."); + return false; + } + + auto& imageInfo = texture->ImageInfoAt(target, level); + *out_imageInfo = &imageInfo; + return true; +} + +// For *TexImage* +bool WebGLTexture::ValidateTexImageSpecification( + TexImageTarget target, uint32_t level, const uvec3& size, + webgl::ImageInfo** const out_imageInfo) { + if (mImmutable) { + mContext->ErrorInvalidOperation("Specified texture is immutable."); + return false; + } + + // Do this early to validate `level`. + webgl::ImageInfo* imageInfo; + if (!ValidateTexImage(mContext, this, target, level, &imageInfo)) + return false; + + if (mTarget == LOCAL_GL_TEXTURE_CUBE_MAP && size.x != size.y) { + mContext->ErrorInvalidValue("Cube map images must be square."); + 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 limits.maxTex2dSize, which is + * neither necessarily lower nor higher than MAX_TEXTURE_SIZE. + * + * Note that limits.maxTex2dSize must be >= than the advertized + * MAX_TEXTURE_SIZE. For simplicity, we advertize MAX_TEXTURE_SIZE as + * limits.maxTex2dSize. + */ + + uint32_t maxWidthHeight = 0; + uint32_t maxDepth = 0; + uint32_t maxLevel = 0; + + const auto& limits = mContext->Limits(); + MOZ_ASSERT(level <= 31); + switch (target.get()) { + case LOCAL_GL_TEXTURE_2D: + maxWidthHeight = limits.maxTex2dSize >> level; + maxDepth = 1; + maxLevel = CeilingLog2(limits.maxTex2dSize); + break; + + case LOCAL_GL_TEXTURE_3D: + maxWidthHeight = limits.maxTex3dSize >> level; + maxDepth = maxWidthHeight; + maxLevel = CeilingLog2(limits.maxTex3dSize); + break; + + case LOCAL_GL_TEXTURE_2D_ARRAY: + maxWidthHeight = limits.maxTex2dSize >> level; + // "The maximum number of layers for two-dimensional array textures + // (depth) must be at least MAX_ARRAY_TEXTURE_LAYERS for all levels." + maxDepth = limits.maxTexArrayLayers; + maxLevel = CeilingLog2(limits.maxTex2dSize); + break; + + default: // cube maps + MOZ_ASSERT(IsCubeMap()); + maxWidthHeight = limits.maxTexCubeSize >> level; + maxDepth = 1; + maxLevel = CeilingLog2(limits.maxTexCubeSize); + break; + } + + if (level > maxLevel) { + mContext->ErrorInvalidValue("Requested level is not supported for target."); + return false; + } + + if (size.x > maxWidthHeight || size.y > maxWidthHeight || size.z > maxDepth) { + mContext->ErrorInvalidValue("Requested size at this level is unsupported."); + 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(size.x) || !IsPowerOfTwo(size.y)) { + mContext->ErrorInvalidValue( + "For level > 0, width and height must be" + " powers of two."); + return false; + } + } + } + + *out_imageInfo = imageInfo; + return true; +} + +// For *TexSubImage* +bool WebGLTexture::ValidateTexImageSelection( + TexImageTarget target, uint32_t level, const uvec3& offset, + const uvec3& size, webgl::ImageInfo** const out_imageInfo) { + webgl::ImageInfo* imageInfo; + if (!ValidateTexImage(mContext, this, target, level, &imageInfo)) + return false; + + if (!imageInfo->IsDefined()) { + mContext->ErrorInvalidOperation( + "The specified TexImage has not yet been" + " specified."); + return false; + } + + const auto totalX = CheckedUint32(offset.x) + size.x; + const auto totalY = CheckedUint32(offset.y) + size.y; + const auto totalZ = CheckedUint32(offset.z) + size.z; + + if (!totalX.isValid() || totalX.value() > imageInfo->mWidth || + !totalY.isValid() || totalY.value() > imageInfo->mHeight || + !totalZ.isValid() || totalZ.value() > imageInfo->mDepth) { + mContext->ErrorInvalidValue( + "Offset+size must be <= the size of the existing" + " specified image."); + return false; + } + + *out_imageInfo = imageInfo; + return true; +} + +static bool ValidateCompressedTexUnpack(WebGLContext* webgl, const uvec3& size, + 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(size.x) / blockWidth; + auto heightInBlocks = CheckedUint32(size.y) / blockHeight; + if (size.x % blockWidth) widthInBlocks += 1; + if (size.y % blockHeight) heightInBlocks += 1; + + const CheckedUint32 blocksPerImage = widthInBlocks * heightInBlocks; + const CheckedUint32 bytesPerImage = bytesPerBlock * blocksPerImage; + const CheckedUint32 bytesNeeded = bytesPerImage * size.z; + + if (!bytesNeeded.isValid()) { + webgl->ErrorOutOfMemory("Overflow while computing the needed buffer size."); + return false; + } + + if (dataSize != bytesNeeded.value()) { + webgl->ErrorInvalidValue( + "Provided buffer's size must match expected size." + " (needs %u, has %zu)", + 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, TexImageTarget target, uint32_t level, + const uvec3& offset, const uvec3& size, webgl::ImageInfo* imageInfo, + bool* const out_expectsInit = nullptr) { + if (out_expectsInit) { + *out_expectsInit = false; + } + if (!imageInfo->mUninitializedSlices) return true; + + if (size.x == imageInfo->mWidth && size.y == imageInfo->mHeight) { + bool expectsInit = false; + auto& isSliceUninit = *imageInfo->mUninitializedSlices; + for (const auto z : IntegerRange(offset.z, offset.z + size.z)) { + if (!isSliceUninit[z]) continue; + expectsInit = true; + isSliceUninit[z] = false; + } + if (out_expectsInit) { + *out_expectsInit = expectsInit; + } + + if (!expectsInit) return true; + + bool hasUninitialized = false; + for (const auto z : IntegerRange(imageInfo->mDepth)) { + hasUninitialized |= isSliceUninit[z]; + } + if (!hasUninitialized) { + imageInfo->mUninitializedSlices = Nothing(); + } + return true; + } + + WebGLContext* webgl = tex->mContext; + webgl->GenerateWarning( + "Texture has not been initialized prior to a" + " partial upload, forcing the browser to clear it." + " This may be slow."); + if (!tex->EnsureImageDataInitialized(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 WebGLContext* webgl, TexImageTarget target, uint32_t level, + const webgl::FormatInfo* format, const uvec3& size) { + const auto fnIsDimValid_S3TC = [&](const char* const name, uint32_t levelSize, + uint32_t blockSize) { + const auto impliedBaseSize = levelSize << level; + if (impliedBaseSize % blockSize == 0) return true; + webgl->ErrorInvalidOperation( + "%u is never a valid %s for level %u, because it implies a base mip %s " + "of %u." + " %s requires that base mip levels have a %s multiple of %u.", + levelSize, name, level, name, impliedBaseSize, format->name, name, + blockSize); + return false; + }; + + 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("TEXTURE_3D requires ASTC's hdr profile."); + return false; + } + break; + + case webgl::CompressionFamily::PVRTC: + if (!IsPowerOfTwo(size.x) || !IsPowerOfTwo(size.y)) { + webgl->ErrorInvalidValue("%s requires power-of-two width and height.", + format->name); + return false; + } + break; + + case webgl::CompressionFamily::BPTC: + case webgl::CompressionFamily::RGTC: + case webgl::CompressionFamily::S3TC: + if (!fnIsDimValid_S3TC("width", size.x, + format->compression->blockWidth) || + !fnIsDimValid_S3TC("height", size.y, + format->compression->blockHeight)) { + return false; + } + break; + + // Default: There are no restrictions on CompressedTexImage. + case webgl::CompressionFamily::ES3: + case webgl::CompressionFamily::ETC1: + break; + } + + return true; +} + +static bool ValidateFormatAndSize(const WebGLContext* webgl, + TexImageTarget target, + const webgl::FormatInfo* format, + const uvec3& size) { + // Check if texture size will likely be rejected by the driver and give a more + // meaningful error message. + auto baseImageSize = CheckedInt<uint64_t>(format->estimatedBytesPerPixel) * + (uint32_t)size.x * (uint32_t)size.y * (uint32_t)size.z; + if (target == LOCAL_GL_TEXTURE_CUBE_MAP) { + baseImageSize *= 6; + } + if (!baseImageSize.isValid() || + baseImageSize.value() > + (uint64_t)StaticPrefs::webgl_max_size_per_texture_mib() * + (1024 * 1024)) { + webgl->ErrorOutOfMemory( + "Texture size too large; base image mebibytes > " + "webgl.max-size-per-texture-mib"); + return false; + } + + // 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." + const bool ok = [&]() { + if (bool(format->d) & (target == LOCAL_GL_TEXTURE_3D)) return false; + + if (format->compression) { + switch (format->compression->family) { + case webgl::CompressionFamily::ES3: + case webgl::CompressionFamily::S3TC: + if (target == LOCAL_GL_TEXTURE_3D) return false; + break; + + case webgl::CompressionFamily::ETC1: + case webgl::CompressionFamily::PVRTC: + case webgl::CompressionFamily::RGTC: + if (target == LOCAL_GL_TEXTURE_3D || + target == LOCAL_GL_TEXTURE_2D_ARRAY) { + return false; + } + break; + default: + break; + } + } + return true; + }(); + if (!ok) { + webgl->ErrorInvalidOperation("Format %s cannot be used with target %s.", + format->name, GetEnumName(target.get())); + return false; + } + + return true; +} + +void WebGLTexture::TexStorage(TexTarget target, uint32_t levels, + GLenum sizedFormat, const uvec3& size) { + // Check levels + if (levels < 1) { + mContext->ErrorInvalidValue("`levels` must be >= 1."); + return; + } + + if (!size.x || !size.y || !size.z) { + mContext->ErrorInvalidValue("Dimensions must be non-zero."); + return; + } + + const TexImageTarget testTarget = + IsCubeMap() ? LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X : target.get(); + webgl::ImageInfo* baseImageInfo; + if (!ValidateTexImageSpecification(testTarget, 0, size, &baseImageInfo)) { + return; + } + MOZ_ALWAYS_TRUE(baseImageInfo); + + auto dstUsage = mContext->mFormatUsage->GetSizedTexUsage(sizedFormat); + if (!dstUsage) { + mContext->ErrorInvalidEnumInfo("internalformat", sizedFormat); + return; + } + auto dstFormat = dstUsage->format; + + if (!ValidateFormatAndSize(mContext, testTarget, dstFormat, size)) return; + + if (dstFormat->compression) { + if (!ValidateCompressedTexImageRestrictions(mContext, testTarget, 0, + dstFormat, size)) { + return; + } + } + + //////////////////////////////////// + + const bool levelsOk = [&]() { + // Right-shift is only defined for bits-1, which is too large anyways. + const auto lastLevel = uint32_t(levels - 1); + if (lastLevel > 31) return false; + + const auto lastLevelWidth = uint32_t(size.x) >> lastLevel; + const auto lastLevelHeight = uint32_t(size.y) >> lastLevel; + + // If these are all zero, then some earlier level was the final 1x1(x1) + // level. + bool ok = lastLevelWidth || lastLevelHeight; + if (target == LOCAL_GL_TEXTURE_3D) { + const auto lastLevelDepth = uint32_t(size.z) >> lastLevel; + ok |= bool(lastLevelDepth); + } + return ok; + }(); + if (!levelsOk) { + mContext->ErrorInvalidOperation( + "Too many levels requested for the given" + " dimensions. (levels: %u, width: %u, height: %u," + " depth: %u)", + levels, size.x, size.y, size.z); + return; + } + + //////////////////////////////////// + // Do the thing! + + GLenum error = DoTexStorage(mContext->gl, target.get(), levels, sizedFormat, + size.x, size.y, size.z); + + mContext->OnDataAllocCall(); + + if (error == LOCAL_GL_OUT_OF_MEMORY) { + mContext->ErrorOutOfMemory("Ran out of memory during texture allocation."); + Truncate(); + return; + } + if (error) { + mContext->GenerateError(error, "Unexpected error from driver."); + const nsPrintfCString call( + "DoTexStorage(0x%04x, %i, 0x%04x, %i,%i,%i) -> 0x%04x", target.get(), + levels, sizedFormat, size.x, size.y, size.z, error); + gfxCriticalError() << "Unexpected error from driver: " + << call.BeginReading(); + return; + } + + //////////////////////////////////// + // Update our specification data. + + auto uninitializedSlices = Some(std::vector<bool>(size.z, true)); + const webgl::ImageInfo newInfo{dstUsage, size.x, size.y, size.z, + std::move(uninitializedSlices)}; + + { + const auto base_level = mBaseMipmapLevel; + mBaseMipmapLevel = 0; + + ImageInfoAtFace(0, 0) = newInfo; + PopulateMipChain(levels - 1); + + mBaseMipmapLevel = base_level; + } + + mImmutable = true; + mImmutableLevelCount = AutoAssertCast(levels); + ClampLevelBaseAndMax(); +} + +//////////////////////////////////////// +// Tex(Sub)Image + +// TexSubImage iff `!respectFormat` +void WebGLTexture::TexImage(uint32_t level, GLenum respecFormat, + const uvec3& offset, const webgl::PackingInfo& pi, + const webgl::TexUnpackBlobDesc& src) { + Maybe<RawBuffer<>> cpuDataView; + if (src.cpuData) { + cpuDataView = Some(RawBuffer<>{src.cpuData->Data()}); + } + const auto srcViewDesc = webgl::TexUnpackBlobDesc{src.imageTarget, + src.size, + src.srcAlphaType, + std::move(cpuDataView), + src.pboOffset, + src.structuredSrcSize, + src.image, + src.sd, + src.dataSurf, + src.unpacking, + src.applyUnpackTransforms}; + + const auto blob = webgl::TexUnpackBlob::Create(srcViewDesc); + if (!blob) { + MOZ_ASSERT(false); + return; + } + + const auto imageTarget = blob->mDesc.imageTarget; + auto size = blob->mDesc.size; + + if (!IsTarget3D(imageTarget)) { + size.z = 1; + } + + //////////////////////////////////// + // Get dest info + + const auto& fua = mContext->mFormatUsage; + const auto fnValidateUnpackEnums = [&]() { + if (!fua->AreUnpackEnumsValid(pi.format, pi.type)) { + mContext->ErrorInvalidEnum("Invalid unpack format/type: %s/%s", + EnumString(pi.format).c_str(), + EnumString(pi.type).c_str()); + return false; + } + return true; + }; + + webgl::ImageInfo* imageInfo; + const webgl::FormatUsageInfo* dstUsage; + if (respecFormat) { + if (!ValidateTexImageSpecification(imageTarget, level, size, &imageInfo)) + return; + MOZ_ASSERT(imageInfo); + + if (!fua->IsInternalFormatEnumValid(respecFormat)) { + mContext->ErrorInvalidValue("Invalid internalformat: 0x%04x", + respecFormat); + return; + } + + dstUsage = fua->GetSizedTexUsage(respecFormat); + if (!dstUsage) { + if (respecFormat != 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." + */ + if (!fnValidateUnpackEnums()) return; + mContext->ErrorInvalidOperation( + "Unsized internalFormat must match" + " unpack format."); + return; + } + + dstUsage = fua->GetUnsizedTexUsage(pi); + } + + if (!dstUsage) { + if (!fnValidateUnpackEnums()) return; + mContext->ErrorInvalidOperation( + "Invalid internalformat/format/type:" + " 0x%04x/0x%04x/0x%04x", + respecFormat, pi.format, pi.type); + return; + } + + const auto& dstFormat = dstUsage->format; + if (!ValidateFormatAndSize(mContext, imageTarget, dstFormat, size)) return; + + if (!mContext->IsWebGL2() && dstFormat->d) { + if (imageTarget != LOCAL_GL_TEXTURE_2D || blob->HasData() || level != 0) { + mContext->ErrorInvalidOperation( + "With format %s, this function may only" + " be called with target=TEXTURE_2D," + " data=null, and level=0.", + dstFormat->name); + return; + } + } + } else { + if (!ValidateTexImageSelection(imageTarget, level, offset, size, + &imageInfo)) { + return; + } + MOZ_ASSERT(imageInfo); + dstUsage = imageInfo->mFormat; + + const auto& dstFormat = dstUsage->format; + if (!mContext->IsWebGL2() && dstFormat->d) { + mContext->ErrorInvalidOperation( + "Function may not be called on a texture of" + " format %s.", + dstFormat->name); + return; + } + } + + //////////////////////////////////// + // Get source info + + const webgl::DriverUnpackInfo* driverUnpackInfo; + if (!dstUsage->IsUnpackValid(pi, &driverUnpackInfo)) { + if (!fnValidateUnpackEnums()) return; + mContext->ErrorInvalidOperation( + "Mismatched internalFormat and format/type:" + " 0x%04x and 0x%04x/0x%04x", + respecFormat, pi.format, pi.type); + return; + } + + if (!blob->Validate(mContext, pi)) return; + + //////////////////////////////////// + // Do the thing! + + Maybe<webgl::ImageInfo> newImageInfo; + bool isRespec = false; + if (respecFormat) { + // It's tempting to do allocation first, and TexSubImage second, but this is + // generally slower. + newImageInfo = Some(webgl::ImageInfo{dstUsage, size.x, size.y, size.z}); + if (!blob->HasData()) { + newImageInfo->mUninitializedSlices = + Some(std::vector<bool>(size.z, true)); + } + + isRespec = (imageInfo->mWidth != newImageInfo->mWidth || + imageInfo->mHeight != newImageInfo->mHeight || + imageInfo->mDepth != newImageInfo->mDepth || + imageInfo->mFormat != newImageInfo->mFormat); + } else { + if (!blob->HasData()) { + mContext->ErrorInvalidValue("`source` cannot be null."); + return; + } + if (!EnsureImageDataInitializedForUpload(this, imageTarget, level, offset, + size, imageInfo)) { + return; + } + } + + webgl::PixelPackingState{}.AssertCurrentUnpack(*mContext->gl, + mContext->IsWebGL2()); + + blob->mDesc.unpacking.ApplyUnpack(*mContext->gl, mContext->IsWebGL2(), size); + const auto revertUnpacking = MakeScopeExit([&]() { + webgl::PixelPackingState{}.ApplyUnpack(*mContext->gl, mContext->IsWebGL2(), + size); + }); + + const bool isSubImage = !respecFormat; + GLenum glError = 0; + if (!blob->TexOrSubImage(isSubImage, isRespec, this, level, driverUnpackInfo, + offset.x, offset.y, offset.z, pi, &glError)) { + return; + } + + if (glError == LOCAL_GL_OUT_OF_MEMORY) { + mContext->ErrorOutOfMemory("Driver ran out of memory during upload."); + Truncate(); + return; + } + + if (glError) { + const auto enumStr = EnumString(glError); + const nsPrintfCString dui( + "Unexpected error %s during upload. (dui: %x/%x/%x)", enumStr.c_str(), + driverUnpackInfo->internalFormat, driverUnpackInfo->unpackFormat, + driverUnpackInfo->unpackType); + mContext->ErrorInvalidOperation("%s", dui.BeginReading()); + gfxCriticalError() << mContext->FuncName() << ": " << dui.BeginReading(); + return; + } + + //////////////////////////////////// + // Update our specification data? + + if (respecFormat) { + mContext->OnDataAllocCall(); + *imageInfo = *newImageInfo; + InvalidateCaches(); + } +} + +//////////////////////////////////////// +// CompressedTex(Sub)Image + +static inline bool IsSubImageBlockAligned( + const webgl::CompressedFormatInfo* compression, + const webgl::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; +} + +// CompressedTexSubImage iff `sub` +void WebGLTexture::CompressedTexImage(bool sub, GLenum imageTarget, + uint32_t level, GLenum formatEnum, + const uvec3& offset, const uvec3& size, + const Range<const uint8_t>& src, + const uint32_t pboImageSize, + const Maybe<uint64_t>& pboOffset) { + auto imageSize = pboImageSize; + if (pboOffset) { + const auto& buffer = + mContext->ValidateBufferSelection(LOCAL_GL_PIXEL_UNPACK_BUFFER); + if (!buffer) return; + auto availBytes = buffer->ByteLength(); + if (*pboOffset > availBytes) { + mContext->GenerateError( + LOCAL_GL_INVALID_OPERATION, + "`offset` (%llu) must be <= PIXEL_UNPACK_BUFFER size (%llu).", + *pboOffset, availBytes); + return; + } + availBytes -= *pboOffset; + if (availBytes < pboImageSize) { + mContext->GenerateError( + LOCAL_GL_INVALID_OPERATION, + "PIXEL_UNPACK_BUFFER size minus `offset` (%llu) too small for" + " `pboImageSize` (%u).", + availBytes, pboImageSize); + return; + } + } else { + if (mContext->mBoundPixelUnpackBuffer) { + mContext->GenerateError(LOCAL_GL_INVALID_OPERATION, + "PIXEL_UNPACK_BUFFER is non-null."); + return; + } + imageSize = src.length(); + } + + // - + + const auto usage = mContext->mFormatUsage->GetSizedTexUsage(formatEnum); + if (!usage || !usage->format->compression) { + mContext->ErrorInvalidEnumArg("format", formatEnum); + return; + } + + webgl::ImageInfo* imageInfo; + if (!sub) { + if (!ValidateTexImageSpecification(imageTarget, level, size, &imageInfo)) { + return; + } + MOZ_ASSERT(imageInfo); + + if (!ValidateFormatAndSize(mContext, imageTarget, usage->format, size)) + return; + if (!ValidateCompressedTexImageRestrictions(mContext, imageTarget, level, + usage->format, size)) { + return; + } + } else { + if (!ValidateTexImageSelection(imageTarget, level, offset, size, + &imageInfo)) + return; + MOZ_ASSERT(imageInfo); + + const auto dstUsage = imageInfo->mFormat; + if (usage != dstUsage) { + mContext->ErrorInvalidOperation( + "`format` must match the format of the" + " existing texture image."); + return; + } + + const auto& format = usage->format; + switch (format->compression->family) { + // Forbidden: + case webgl::CompressionFamily::ETC1: + mContext->ErrorInvalidOperation( + "Format does not allow sub-image" + " updates."); + return; + + // Block-aligned: + case webgl::CompressionFamily::ES3: // Yes, the ES3 formats don't match + // the ES3 + case webgl::CompressionFamily::S3TC: // default behavior. + case webgl::CompressionFamily::BPTC: + case webgl::CompressionFamily::RGTC: + if (!IsSubImageBlockAligned(format->compression, imageInfo, offset.x, + offset.y, size.x, size.y)) { + mContext->ErrorInvalidOperation( + "Format requires block-aligned sub-image" + " updates."); + return; + } + break; + + // Full-only: (The ES3 default) + case webgl::CompressionFamily::ASTC: + case webgl::CompressionFamily::PVRTC: + if (offset.x || offset.y || size.x != imageInfo->mWidth || + size.y != imageInfo->mHeight) { + mContext->ErrorInvalidOperation( + "Format does not allow partial sub-image" + " updates."); + return; + } + break; + } + } + + switch (usage->format->compression->family) { + case webgl::CompressionFamily::BPTC: + case webgl::CompressionFamily::RGTC: + if (level == 0) { + if (size.x % 4 != 0 || size.y % 4 != 0) { + mContext->ErrorInvalidOperation( + "For level == 0, width and height must be multiples of 4."); + return; + } + } + break; + + default: + break; + } + + if (!ValidateCompressedTexUnpack(mContext, size, usage->format, imageSize)) + return; + + //////////////////////////////////// + // Do the thing! + + if (sub) { + if (!EnsureImageDataInitializedForUpload(this, imageTarget, level, offset, + size, imageInfo)) { + return; + } + } + + const ScopedLazyBind bindPBO(mContext->gl, LOCAL_GL_PIXEL_UNPACK_BUFFER, + mContext->mBoundPixelUnpackBuffer); + GLenum error; + const void* ptr; + if (pboOffset) { + ptr = reinterpret_cast<const void*>(*pboOffset); + } else { + ptr = reinterpret_cast<const void*>(src.begin().get()); + } + + if (!sub) { + error = DoCompressedTexImage(mContext->gl, imageTarget, level, formatEnum, + size.x, size.y, size.z, imageSize, ptr); + } else { + error = DoCompressedTexSubImage(mContext->gl, imageTarget, level, offset.x, + offset.y, offset.z, size.x, size.y, size.z, + formatEnum, imageSize, ptr); + } + if (error == LOCAL_GL_OUT_OF_MEMORY) { + mContext->ErrorOutOfMemory("Ran out of memory during upload."); + Truncate(); + return; + } + if (error) { + mContext->GenerateError(error, "Unexpected error from driver."); + nsCString call; + if (!sub) { + call = nsPrintfCString( + "DoCompressedTexImage(0x%04x, %u, 0x%04x, %u,%u,%u, %u, %p)", + imageTarget, level, formatEnum, size.x, size.y, size.z, imageSize, + ptr); + } else { + call = nsPrintfCString( + "DoCompressedTexSubImage(0x%04x, %u, %u,%u,%u, %u,%u,%u, 0x%04x, %u, " + "%p)", + imageTarget, level, offset.x, offset.y, offset.z, size.x, size.y, + size.z, formatEnum, imageSize, ptr); + } + gfxCriticalError() << "Unexpected error " << gfx::hexa(error) + << " from driver: " << call.BeginReading(); + return; + } + + //////////////////////////////////// + // Update our specification data? + + if (!sub) { + const auto uninitializedSlices = Nothing(); + const webgl::ImageInfo newImageInfo{usage, size.x, size.y, size.z, + uninitializedSlices}; + *imageInfo = newImageInfo; + InvalidateCaches(); + } +} + +//////////////////////////////////////// +// CopyTex(Sub)Image + +static bool ValidateCopyTexImageFormats(WebGLContext* webgl, + const webgl::FormatInfo* srcFormat, + const webgl::FormatInfo* dstFormat) { + MOZ_ASSERT(!srcFormat->compression); + if (dstFormat->compression) { + webgl->ErrorInvalidEnum( + "Specified destination must not have a compressed" + " format."); + return false; + } + + if (dstFormat->effectiveFormat == webgl::EffectiveFormat::RGB9_E5) { + webgl->ErrorInvalidOperation( + "RGB9_E5 is an invalid destination for" + " CopyTex(Sub)Image. (GLES 3.0.4 p145)"); + return false; + } + + if (!DoChannelsMatchForCopyTexImage(srcFormat, dstFormat)) { + webgl->ErrorInvalidOperation( + "Destination channels must be compatible with" + " source channels. (GLES 3.0.4 p140 Table 3.16)"); + return false; + } + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// + +class ScopedCopyTexImageSource { + WebGLContext* const mWebGL; + GLuint mRB; + GLuint mFB; + + public: + ScopedCopyTexImageSource(WebGLContext* webgl, uint32_t srcWidth, + uint32_t srcHeight, + const webgl::FormatInfo* srcFormat, + const webgl::FormatUsageInfo* dstUsage); + ~ScopedCopyTexImageSource(); +}; + +ScopedCopyTexImageSource::ScopedCopyTexImageSource( + WebGLContext* webgl, 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( + "Copying to a LUMINANCE, ALPHA, or LUMINANCE_ALPHA" + " is deprecated, and has severely reduced performance" + " on some platforms."); + 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; + webgl->WarnIfImplicit(WebGLExtensionID::WEBGL_color_buffer_float); + break; + } + + if (webgl->IsExtensionEnabled( + WebGLExtensionID::EXT_color_buffer_half_float)) { + sizedFormat = LOCAL_GL_RGBA16F; + webgl->WarnIfImplicit(WebGLExtensionID::EXT_color_buffer_half_float); + 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; + GLuint rgbaFB = 0; + { + gl->fGenRenderbuffers(1, &rgbaRB); + gl::ScopedBindRenderbuffer scopedRB(gl, rgbaRB); + gl->fRenderbufferStorage(LOCAL_GL_RENDERBUFFER, sizedFormat, srcWidth, + srcHeight); + + 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."); + } + } + + // Draw-blit rgbaTex into rgbaFB. + const gfx::IntSize srcSize(srcWidth, srcHeight); + { + const gl::ScopedBindFramebuffer bindFB(gl, rgbaFB); + gl->BlitHelper()->DrawBlitTextureToFramebuffer(scopedTex.Texture(), srcSize, + srcSize); + } + + // 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( + WebGLContext* webgl, const webgl::FormatInfo* srcFormat, + GLenum internalFormat) { + const auto& fua = webgl->mFormatUsage; + + switch (internalFormat) { + case LOCAL_GL_R8_SNORM: + case LOCAL_GL_RG8_SNORM: + case LOCAL_GL_RGB8_SNORM: + case LOCAL_GL_RGBA8_SNORM: + webgl->ErrorInvalidEnum("SNORM formats are invalid for CopyTexImage."); + return nullptr; + } + + auto dstUsage = fua->GetSizedTexUsage(internalFormat); + if (!dstUsage) { + // Ok, maybe it's unsized. + webgl::UnsizedFormat unsizedFormat; + if (!GetUnsizedFormatForCopy(internalFormat, &unsizedFormat)) { + webgl->ErrorInvalidEnumInfo("internalFormat", internalFormat); + return nullptr; + } + + const auto dstFormat = srcFormat->GetCopyDecayFormat(unsizedFormat); + if (dstFormat) { + dstUsage = fua->GetUsage(dstFormat->effectiveFormat); + } + if (!dstUsage) { + webgl->ErrorInvalidOperation( + "0x%04x is not a valid unsized format for" + " source format %s.", + internalFormat, srcFormat->name); + return nullptr; + } + + return dstUsage; + } + // Alright, it's sized. + + const auto dstFormat = dstUsage->format; + + if (dstFormat->componentType != srcFormat->componentType) { + webgl->ErrorInvalidOperation( + "For sized internalFormats, source and dest" + " component types must match. (source: %s, dest:" + " %s)", + 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( + "For sized internalFormats, source and dest" + " component sizes must match exactly. (source: %s," + " dest: %s)", + srcFormat->name, dstFormat->name); + return nullptr; + } + + return dstUsage; +} + +static bool ValidateCopyTexImageForFeedback(const WebGLContext& webgl, + const WebGLTexture& tex, + const uint32_t mipLevel, + const uint32_t zLayer) { + const auto& fb = webgl.BoundReadFb(); + if (fb) { + MOZ_ASSERT(fb->ColorReadBuffer()); + const auto& attach = *fb->ColorReadBuffer(); + MOZ_ASSERT(attach.ZLayerCount() == + 1); // Multiview invalid for copyTexImage. + + if (attach.Texture() == &tex && attach.Layer() == zLayer && + attach.MipLevel() == mipLevel) { + // 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. + webgl.ErrorInvalidOperation( + "Feedback loop detected, as this texture" + " is already attached to READ_FRAMEBUFFER's" + " READ_BUFFER-selected COLOR_ATTACHMENT%u.", + attach.mAttachmentPoint); + return false; + } + } + return true; +} + +static bool DoCopyTexOrSubImage(WebGLContext* webgl, bool isSubImage, + bool needsInit, WebGLTexture* const tex, + const 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("Bad subrect selection."); + return false; + } + + writeX += xOffset; + writeY += yOffset; + + //// + + GLenum error = 0; + nsCString errorText; + do { + const auto& idealUnpack = dstUsage->idealUnpack; + const auto& pi = idealUnpack->ToPacking(); + + UniqueBuffer zeros; + const bool fullOverwrite = + (uint32_t(rwWidth) == dstWidth && uint32_t(rwHeight) == dstHeight); + if (needsInit && !fullOverwrite) { + CheckedInt<size_t> byteCount = BytesPerPixel(pi); + byteCount *= dstWidth; + byteCount *= dstHeight; + + if (byteCount.isValid()) { + zeros = UniqueBuffer::Take(calloc(1u, byteCount.value())); + } + + if (!zeros.get()) { + webgl->ErrorOutOfMemory("Ran out of memory allocating zeros."); + return false; + } + } + + if (!isSubImage || zeros) { + webgl::PixelPackingState{}.AssertCurrentUnpack(*gl, webgl->IsWebGL2()); + + gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 1); + const auto revert = MakeScopeExit( + [&]() { gl->fPixelStorei(LOCAL_GL_UNPACK_ALIGNMENT, 4); }); + if (!isSubImage) { + error = DoTexImage(gl, target, level, idealUnpack, dstWidth, dstHeight, + 1, nullptr); + if (error) { + errorText = nsPrintfCString( + "DoTexImage(0x%04x, %i, {0x%04x, 0x%04x, 0x%04x}, %u,%u,1) -> " + "0x%04x", + target.get(), level, idealUnpack->internalFormat, + idealUnpack->unpackFormat, idealUnpack->unpackType, dstWidth, + dstHeight, error); + break; + } + } + if (zeros) { + error = DoTexSubImage(gl, target, level, xOffset, yOffset, zOffset, + dstWidth, dstHeight, 1, pi, zeros.get()); + if (error) { + errorText = nsPrintfCString( + "DoTexSubImage(0x%04x, %i, %i,%i,%i, %u,%u,1, {0x%04x, 0x%04x}) " + "-> " + "0x%04x", + target.get(), level, xOffset, yOffset, zOffset, dstWidth, + dstHeight, idealUnpack->unpackFormat, idealUnpack->unpackType, + 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, srcTotalWidth, srcTotalHeight, + srcFormat, dstUsage); + + error = DoCopyTexSubImage(gl, target, level, writeX, writeY, zOffset, readX, + readY, rwWidth, rwHeight); + if (error) { + errorText = nsPrintfCString( + "DoCopyTexSubImage(0x%04x, %i, %i,%i,%i, %i,%i, %u,%u) -> 0x%04x", + target.get(), level, writeX, writeY, zOffset, readX, readY, rwWidth, + rwHeight, error); + break; + } + + return true; + } while (false); + + if (error == LOCAL_GL_OUT_OF_MEMORY) { + webgl->ErrorOutOfMemory("Ran out of memory during texture copy."); + tex->Truncate(); + return false; + } + + if (gl->IsANGLE() && error == LOCAL_GL_INVALID_OPERATION) { + webgl->ErrorImplementationBug( + "ANGLE is particular about CopyTexSubImage" + " formats matching exactly."); + return false; + } + + webgl->GenerateError(error, "Unexpected error from driver."); + gfxCriticalError() << "Unexpected error from driver: " + << errorText.BeginReading(); + return false; +} + +// CopyTexSubImage if `!respecFormat` +void WebGLTexture::CopyTexImage(GLenum imageTarget, uint32_t level, + GLenum respecFormat, const uvec3& dstOffset, + const ivec2& srcOffset, const uvec2& size2) { + //////////////////////////////////// + // Get source info + + const webgl::FormatUsageInfo* srcUsage; + uint32_t srcTotalWidth; + uint32_t srcTotalHeight; + if (!mContext->BindCurFBForColorRead(&srcUsage, &srcTotalWidth, + &srcTotalHeight)) { + return; + } + const auto& srcFormat = srcUsage->format; + + if (!ValidateCopyTexImageForFeedback(*mContext, *this, level, dstOffset.z)) + return; + + const auto size = uvec3{size2.x, size2.y, 1}; + + //////////////////////////////////// + // Get dest info + + webgl::ImageInfo* imageInfo; + const webgl::FormatUsageInfo* dstUsage; + if (respecFormat) { + if (!ValidateTexImageSpecification(imageTarget, level, size, &imageInfo)) + return; + MOZ_ASSERT(imageInfo); + + dstUsage = ValidateCopyDestUsage(mContext, srcFormat, respecFormat); + if (!dstUsage) return; + + if (!ValidateFormatAndSize(mContext, imageTarget, dstUsage->format, size)) + return; + } else { + if (!ValidateTexImageSelection(imageTarget, level, dstOffset, size, + &imageInfo)) { + return; + } + MOZ_ASSERT(imageInfo); + + dstUsage = imageInfo->mFormat; + MOZ_ASSERT(dstUsage); + } + + const auto& dstFormat = dstUsage->format; + if (!mContext->IsWebGL2() && dstFormat->d) { + mContext->ErrorInvalidOperation( + "Function may not be called with format %s.", dstFormat->name); + return; + } + + //////////////////////////////////// + // Check that source and dest info are compatible + + if (!ValidateCopyTexImageFormats(mContext, srcFormat, dstFormat)) return; + + //////////////////////////////////// + // Do the thing! + + const bool isSubImage = !respecFormat; + bool expectsInit = true; + if (isSubImage) { + if (!EnsureImageDataInitializedForUpload(this, imageTarget, level, + dstOffset, size, imageInfo, + &expectsInit)) { + return; + } + } + + if (!DoCopyTexOrSubImage(mContext, isSubImage, expectsInit, this, imageTarget, + level, srcOffset.x, srcOffset.y, srcTotalWidth, + srcTotalHeight, srcUsage, dstOffset.x, dstOffset.y, + dstOffset.z, size.x, size.y, dstUsage)) { + Truncate(); + return; + } + + mContext->OnDataAllocCall(); + + //////////////////////////////////// + // Update our specification data? + + if (respecFormat) { + const auto uninitializedSlices = Nothing(); + const webgl::ImageInfo newImageInfo{dstUsage, size.x, size.y, size.z, + uninitializedSlices}; + *imageInfo = newImageInfo; + InvalidateCaches(); + } +} + +} // namespace mozilla |