/* -*- 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 #include #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 FromImageBitmap(const GLenum target, Maybe 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 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 FromOffscreenCanvas( const ClientWebGLContext& webgl, const GLenum target, Maybe 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(&canvas), flags); RefPtr 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 FromDomElem(const ClientWebGLContext& webgl, const GLenum target, Maybe size, const dom::Element& elem, ErrorResult* const out_error) { if (elem.IsHTMLElement(nsGkAtoms::canvas)) { const dom::HTMLCanvasElement* srcCanvas = static_cast(&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 idealDrawTarget = nullptr; // Don't care for now. auto sfer = nsLayoutUtils::SurfaceFromElement( const_cast(&elem), flags, idealDrawTarget); ////// uvec2 elemSize; const auto& layersImage = sfer.mLayersImage; Maybe 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 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(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(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> 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 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(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& src, const uint32_t pboImageSize, const Maybe& 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(*pboOffset); } else { ptr = reinterpret_cast(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 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 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