diff -r 000000000000 -r 6474c204b198 content/canvas/src/WebGLTexture.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/content/canvas/src/WebGLTexture.cpp Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,485 @@ +/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "WebGLContext.h" +#include "WebGLContextUtils.h" +#include "WebGLTexture.h" +#include "GLContext.h" +#include "ScopedGLHelpers.h" +#include "WebGLTexelConversions.h" +#include "mozilla/dom/WebGLRenderingContextBinding.h" +#include + +using namespace mozilla; + +JSObject* +WebGLTexture::WrapObject(JSContext *cx) { + return dom::WebGLTextureBinding::Wrap(cx, this); +} + +WebGLTexture::WebGLTexture(WebGLContext *context) + : WebGLContextBoundObject(context) + , mHasEverBeenBound(false) + , mTarget(0) + , mMinFilter(LOCAL_GL_NEAREST_MIPMAP_LINEAR) + , mMagFilter(LOCAL_GL_LINEAR) + , mWrapS(LOCAL_GL_REPEAT) + , mWrapT(LOCAL_GL_REPEAT) + , mFacesCount(0) + , mMaxLevelWithCustomImages(0) + , mHaveGeneratedMipmap(false) + , mFakeBlackStatus(WebGLTextureFakeBlackStatus::IncompleteTexture) +{ + SetIsDOMBinding(); + mContext->MakeContextCurrent(); + mContext->gl->fGenTextures(1, &mGLName); + mContext->mTextures.insertBack(this); +} + +void +WebGLTexture::Delete() { + mImageInfos.Clear(); + mContext->MakeContextCurrent(); + mContext->gl->fDeleteTextures(1, &mGLName); + LinkedListElement::removeFrom(mContext->mTextures); +} + +int64_t +WebGLTexture::ImageInfo::MemoryUsage() const { + if (mImageDataStatus == WebGLImageDataStatus::NoImageData) + return 0; + int64_t bitsPerTexel = WebGLContext::GetBitsPerTexel(mWebGLFormat, mWebGLType); + return int64_t(mWidth) * int64_t(mHeight) * bitsPerTexel/8; +} + +int64_t +WebGLTexture::MemoryUsage() const { + if (IsDeleted()) + return 0; + int64_t result = 0; + for(size_t face = 0; face < mFacesCount; face++) { + if (mHaveGeneratedMipmap) { + // Each mipmap level is 1/4 the size of the previous level + // 1 + x + x^2 + ... = 1/(1-x) + // for x = 1/4, we get 1/(1-1/4) = 4/3 + result += ImageInfoAtFace(face, 0).MemoryUsage() * 4 / 3; + } else { + for(size_t level = 0; level <= mMaxLevelWithCustomImages; level++) + result += ImageInfoAtFace(face, level).MemoryUsage(); + } + } + return result; +} + +bool +WebGLTexture::DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(GLenum texImageTarget) const { + if (mHaveGeneratedMipmap) + return true; + + // We want a copy here so we can modify it temporarily. + ImageInfo expected = ImageInfoAt(texImageTarget, 0); + + // checks if custom level>0 images are all defined up to the highest level defined + // and have the expected dimensions + for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) { + const ImageInfo& actual = ImageInfoAt(texImageTarget, level); + if (actual != expected) + return false; + expected.mWidth = std::max(1, expected.mWidth >> 1); + expected.mHeight = std::max(1, expected.mHeight >> 1); + + // if the current level has size 1x1, we can stop here: the spec doesn't seem to forbid the existence + // of extra useless levels. + if (actual.mWidth == 1 && actual.mHeight == 1) + return true; + } + + // if we're here, we've exhausted all levels without finding a 1x1 image + return false; +} + +void +WebGLTexture::Bind(GLenum aTarget) { + // this function should only be called by bindTexture(). + // it assumes that the GL context is already current. + + bool firstTimeThisTextureIsBound = !mHasEverBeenBound; + + if (!firstTimeThisTextureIsBound && aTarget != mTarget) { + mContext->ErrorInvalidOperation("bindTexture: this texture has already been bound to a different target"); + // very important to return here before modifying texture state! This was the place when I lost a whole day figuring + // very strange 'invalid write' crashes. + return; + } + + mTarget = aTarget; + + mContext->gl->fBindTexture(mTarget, mGLName); + + if (firstTimeThisTextureIsBound) { + mFacesCount = (mTarget == LOCAL_GL_TEXTURE_2D) ? 1 : 6; + EnsureMaxLevelWithCustomImagesAtLeast(0); + SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown); + + // thanks to the WebKit people for finding this out: GL_TEXTURE_WRAP_R is not + // present in GLES 2, but is present in GL and it seems as if for cube maps + // we need to set it to GL_CLAMP_TO_EDGE to get the expected GLES behavior. + if (mTarget == LOCAL_GL_TEXTURE_CUBE_MAP && !mContext->gl->IsGLES()) + mContext->gl->fTexParameteri(mTarget, LOCAL_GL_TEXTURE_WRAP_R, LOCAL_GL_CLAMP_TO_EDGE); + } + + mHasEverBeenBound = true; +} + +void +WebGLTexture::SetImageInfo(GLenum aTarget, GLint aLevel, + GLsizei aWidth, GLsizei aHeight, + GLenum aFormat, GLenum aType, WebGLImageDataStatus aStatus) +{ + if ( (aTarget == LOCAL_GL_TEXTURE_2D) != (mTarget == LOCAL_GL_TEXTURE_2D) ) + return; + + EnsureMaxLevelWithCustomImagesAtLeast(aLevel); + + ImageInfoAt(aTarget, aLevel) = ImageInfo(aWidth, aHeight, aFormat, aType, aStatus); + + if (aLevel > 0) + SetCustomMipmap(); + + // Invalidate framebuffer status cache + NotifyFBsStatusChanged(); + + SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown); +} + +void +WebGLTexture::SetGeneratedMipmap() { + if (!mHaveGeneratedMipmap) { + mHaveGeneratedMipmap = true; + SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown); + } +} + +void +WebGLTexture::SetCustomMipmap() { + if (mHaveGeneratedMipmap) { + // if we were in GeneratedMipmap mode and are now switching to CustomMipmap mode, + // we need to compute now all the mipmap image info. + + // since we were in GeneratedMipmap mode, we know that the level 0 images all have the same info, + // and are power-of-two. + ImageInfo imageInfo = ImageInfoAtFace(0, 0); + NS_ASSERTION(imageInfo.IsPowerOfTwo(), "this texture is NPOT, so how could GenerateMipmap() ever accept it?"); + + GLsizei size = std::max(imageInfo.mWidth, imageInfo.mHeight); + + // so, the size is a power of two, let's find its log in base 2. + size_t maxLevel = 0; + for (GLsizei n = size; n > 1; n >>= 1) + ++maxLevel; + + EnsureMaxLevelWithCustomImagesAtLeast(maxLevel); + + for (size_t level = 1; level <= maxLevel; ++level) { + // again, since the sizes are powers of two, no need for any max(1,x) computation + imageInfo.mWidth >>= 1; + imageInfo.mHeight >>= 1; + for(size_t face = 0; face < mFacesCount; ++face) + ImageInfoAtFace(face, level) = imageInfo; + } + } + mHaveGeneratedMipmap = false; +} + +bool +WebGLTexture::AreAllLevel0ImageInfosEqual() const { + for (size_t face = 1; face < mFacesCount; ++face) { + if (ImageInfoAtFace(face, 0) != ImageInfoAtFace(0, 0)) + return false; + } + return true; +} + +bool +WebGLTexture::IsMipmapTexture2DComplete() const { + if (mTarget != LOCAL_GL_TEXTURE_2D) + return false; + if (!ImageInfoAt(LOCAL_GL_TEXTURE_2D, 0).IsPositive()) + return false; + if (mHaveGeneratedMipmap) + return true; + return DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(LOCAL_GL_TEXTURE_2D); +} + +bool +WebGLTexture::IsCubeComplete() const { + if (mTarget != LOCAL_GL_TEXTURE_CUBE_MAP) + return false; + const ImageInfo &first = ImageInfoAt(LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0); + if (!first.IsPositive() || !first.IsSquare()) + return false; + return AreAllLevel0ImageInfosEqual(); +} + +static GLenum +GLCubeMapFaceById(int id) +{ + GLenum result = LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + id; + MOZ_ASSERT(result >= LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X && + result <= LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z); + return result; +} + +bool +WebGLTexture::IsMipmapCubeComplete() const { + if (!IsCubeComplete()) // in particular, this checks that this is a cube map + return false; + for (int i = 0; i < 6; i++) { + GLenum face = GLCubeMapFaceById(i); + if (!DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(face)) + return false; + } + return true; +} + +WebGLTextureFakeBlackStatus +WebGLTexture::ResolvedFakeBlackStatus() { + if (MOZ_LIKELY(mFakeBlackStatus != WebGLTextureFakeBlackStatus::Unknown)) { + return mFakeBlackStatus; + } + + // Determine if the texture needs to be faked as a black texture. + // See 3.8.2 Shader Execution in the OpenGL ES 2.0.24 spec. + + for (size_t face = 0; face < mFacesCount; ++face) { + if (ImageInfoAtFace(face, 0).mImageDataStatus == WebGLImageDataStatus::NoImageData) { + // In case of undefined texture image, we don't print any message because this is a very common + // and often legitimate case (asynchronous texture loading). + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + return mFakeBlackStatus; + } + } + + const char *msg_rendering_as_black + = "A texture is going to be rendered as if it were black, as per the OpenGL ES 2.0.24 spec section 3.8.2, " + "because it"; + + if (mTarget == LOCAL_GL_TEXTURE_2D) + { + if (DoesMinFilterRequireMipmap()) + { + if (!IsMipmapTexture2DComplete()) { + mContext->GenerateWarning + ("%s is a 2D texture, with a minification filter requiring a mipmap, " + "and is not mipmap complete (as defined in section 3.7.10).", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } else if (!ImageInfoAt(mTarget, 0).IsPowerOfTwo()) { + mContext->GenerateWarning + ("%s is a 2D texture, with a minification filter requiring a mipmap, " + "and either its width or height is not a power of two.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } + else // no mipmap required + { + if (!ImageInfoAt(mTarget, 0).IsPositive()) { + mContext->GenerateWarning + ("%s is a 2D texture and its width or height is equal to zero.", + msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } else if (!AreBothWrapModesClampToEdge() && !ImageInfoAt(mTarget, 0).IsPowerOfTwo()) { + mContext->GenerateWarning + ("%s is a 2D texture, with a minification filter not requiring a mipmap, " + "with its width or height not a power of two, and with a wrap mode " + "different from CLAMP_TO_EDGE.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } + } + else // cube map + { + bool areAllLevel0ImagesPOT = true; + for (size_t face = 0; face < mFacesCount; ++face) + areAllLevel0ImagesPOT &= ImageInfoAtFace(face, 0).IsPowerOfTwo(); + + if (DoesMinFilterRequireMipmap()) + { + if (!IsMipmapCubeComplete()) { + mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, " + "and is not mipmap cube complete (as defined in section 3.7.10).", + msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } else if (!areAllLevel0ImagesPOT) { + mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, " + "and either the width or the height of some level 0 image is not a power of two.", + msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } + else // no mipmap required + { + if (!IsCubeComplete()) { + mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, " + "and is not cube complete (as defined in section 3.7.10).", + msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } else if (!AreBothWrapModesClampToEdge() && !areAllLevel0ImagesPOT) { + mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, " + "with some level 0 image having width or height not a power of two, and with a wrap mode " + "different from CLAMP_TO_EDGE.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } + } + + if (ImageInfoBase().mWebGLType == LOCAL_GL_FLOAT && + !Context()->IsExtensionEnabled(WebGLExtensionID::OES_texture_float_linear)) + { + if (mMinFilter == LOCAL_GL_LINEAR || + mMinFilter == LOCAL_GL_LINEAR_MIPMAP_LINEAR || + mMinFilter == LOCAL_GL_LINEAR_MIPMAP_NEAREST || + mMinFilter == LOCAL_GL_NEAREST_MIPMAP_LINEAR) + { + mContext->GenerateWarning("%s is a texture with a linear minification filter, " + "which is not compatible with gl.FLOAT by default. " + "Try enabling the OES_texture_float_linear extension if supported.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + else if (mMagFilter == LOCAL_GL_LINEAR) + { + mContext->GenerateWarning("%s is a texture with a linear magnification filter, " + "which is not compatible with gl.FLOAT by default. " + "Try enabling the OES_texture_float_linear extension if supported.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } else if (ImageInfoBase().mWebGLType == LOCAL_GL_HALF_FLOAT_OES && + !Context()->IsExtensionEnabled(WebGLExtensionID::OES_texture_half_float_linear)) + { + if (mMinFilter == LOCAL_GL_LINEAR || + mMinFilter == LOCAL_GL_LINEAR_MIPMAP_LINEAR || + mMinFilter == LOCAL_GL_LINEAR_MIPMAP_NEAREST || + mMinFilter == LOCAL_GL_NEAREST_MIPMAP_LINEAR) + { + mContext->GenerateWarning("%s is a texture with a linear minification filter, " + "which is not compatible with gl.HALF_FLOAT by default. " + "Try enabling the OES_texture_half_float_linear extension if supported.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + else if (mMagFilter == LOCAL_GL_LINEAR) + { + mContext->GenerateWarning("%s is a texture with a linear magnification filter, " + "which is not compatible with gl.HALF_FLOAT by default. " + "Try enabling the OES_texture_half_float_linear extension if supported.", msg_rendering_as_black); + mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture; + } + } + + // We have exhausted all cases of incomplete textures, where we would need opaque black. + // We may still need transparent black in case of uninitialized image data. + bool hasUninitializedImageData = false; + for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) { + for (size_t face = 0; face < mFacesCount; ++face) { + hasUninitializedImageData |= (ImageInfoAtFace(face, level).mImageDataStatus == WebGLImageDataStatus::UninitializedImageData); + } + } + + if (hasUninitializedImageData) { + bool hasAnyInitializedImageData = false; + for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) { + for (size_t face = 0; face < mFacesCount; ++face) { + if (ImageInfoAtFace(face, level).mImageDataStatus == WebGLImageDataStatus::InitializedImageData) { + hasAnyInitializedImageData = true; + break; + } + } + if (hasAnyInitializedImageData) { + break; + } + } + + if (hasAnyInitializedImageData) { + // The texture contains some initialized image data, and some uninitialized image data. + // In this case, we have no choice but to initialize all image data now. Fortunately, + // in this case we know that we can't be dealing with a depth texture per WEBGL_depth_texture + // and ANGLE_depth_texture (which allow only one image per texture) so we can assume that + // glTexImage2D is able to upload data to images. + for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) { + for (size_t face = 0; face < mFacesCount; ++face) { + GLenum imageTarget = mTarget == LOCAL_GL_TEXTURE_2D + ? LOCAL_GL_TEXTURE_2D + : LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + face; + const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level); + if (imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData) { + DoDeferredImageInitialization(imageTarget, level); + } + } + } + mFakeBlackStatus = WebGLTextureFakeBlackStatus::NotNeeded; + } else { + // The texture only contains uninitialized image data. In this case, + // we can use a black texture for it. + mFakeBlackStatus = WebGLTextureFakeBlackStatus::UninitializedImageData; + } + } + + // we have exhausted all cases where we do need fakeblack, so if the status is still unknown, + // that means that we do NOT need it. + if (mFakeBlackStatus == WebGLTextureFakeBlackStatus::Unknown) { + mFakeBlackStatus = WebGLTextureFakeBlackStatus::NotNeeded; + } + + MOZ_ASSERT(mFakeBlackStatus != WebGLTextureFakeBlackStatus::Unknown); + return mFakeBlackStatus; +} + +void +WebGLTexture::DoDeferredImageInitialization(GLenum imageTarget, GLint level) +{ + const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level); + MOZ_ASSERT(imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData); + + mContext->MakeContextCurrent(); + gl::ScopedBindTexture autoBindTex(mContext->gl, GLName(), mTarget); + + GLenum format = imageInfo.mWebGLFormat; + GLenum type = imageInfo.mWebGLType; + WebGLTexelFormat texelformat = GetWebGLTexelFormat(format, type); + uint32_t texelsize = WebGLTexelConversions::TexelBytesForFormat(texelformat); + CheckedUint32 checked_byteLength + = WebGLContext::GetImageSize( + imageInfo.mHeight, + imageInfo.mWidth, + texelsize, + mContext->mPixelStoreUnpackAlignment); + MOZ_ASSERT(checked_byteLength.isValid()); // should have been checked earlier + void *zeros = calloc(1, checked_byteLength.value()); + + gl::GLContext* gl = mContext->gl; + GLenum driverType = DriverTypeFromType(gl, type); + GLenum driverInternalFormat = LOCAL_GL_NONE; + GLenum driverFormat = LOCAL_GL_NONE; + DriverFormatsFromFormatAndType(gl, format, type, &driverInternalFormat, &driverFormat); + + mContext->GetAndFlushUnderlyingGLErrors(); + gl->fTexImage2D(imageTarget, level, driverInternalFormat, + imageInfo.mWidth, imageInfo.mHeight, + 0, driverFormat, driverType, + zeros); + GLenum error = mContext->GetAndFlushUnderlyingGLErrors(); + + free(zeros); + SetImageDataStatus(imageTarget, level, WebGLImageDataStatus::InitializedImageData); + + if (error) { + // Should only be OUT_OF_MEMORY. Anyway, there's no good way to recover from this here. + MOZ_CRASH(); // errors on texture upload have been related to video memory exposure in the past. + return; + } +} + +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WebGLTexture) + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WebGLTexture, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WebGLTexture, Release)