content/canvas/src/WebGLTexture.cpp

Thu, 15 Jan 2015 21:03:48 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 15 Jan 2015 21:03:48 +0100
branch
TOR_BUG_9701
changeset 11
deefc01c0e14
permissions
-rw-r--r--

Integrate friendly tips from Tor colleagues to make (or not) 4.5 alpha 3;
This includes removal of overloaded (but unused) methods, and addition of
a overlooked call to DataStruct::SetData(nsISupports, uint32_t, bool.)

     1 /* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
     2 /* This Source Code Form is subject to the terms of the Mozilla Public
     3  * License, v. 2.0. If a copy of the MPL was not distributed with this
     4  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     6 #include "WebGLContext.h"
     7 #include "WebGLContextUtils.h"
     8 #include "WebGLTexture.h"
     9 #include "GLContext.h"
    10 #include "ScopedGLHelpers.h"
    11 #include "WebGLTexelConversions.h"
    12 #include "mozilla/dom/WebGLRenderingContextBinding.h"
    13 #include <algorithm>
    15 using namespace mozilla;
    17 JSObject*
    18 WebGLTexture::WrapObject(JSContext *cx) {
    19     return dom::WebGLTextureBinding::Wrap(cx, this);
    20 }
    22 WebGLTexture::WebGLTexture(WebGLContext *context)
    23     : WebGLContextBoundObject(context)
    24     , mHasEverBeenBound(false)
    25     , mTarget(0)
    26     , mMinFilter(LOCAL_GL_NEAREST_MIPMAP_LINEAR)
    27     , mMagFilter(LOCAL_GL_LINEAR)
    28     , mWrapS(LOCAL_GL_REPEAT)
    29     , mWrapT(LOCAL_GL_REPEAT)
    30     , mFacesCount(0)
    31     , mMaxLevelWithCustomImages(0)
    32     , mHaveGeneratedMipmap(false)
    33     , mFakeBlackStatus(WebGLTextureFakeBlackStatus::IncompleteTexture)
    34 {
    35     SetIsDOMBinding();
    36     mContext->MakeContextCurrent();
    37     mContext->gl->fGenTextures(1, &mGLName);
    38     mContext->mTextures.insertBack(this);
    39 }
    41 void
    42 WebGLTexture::Delete() {
    43     mImageInfos.Clear();
    44     mContext->MakeContextCurrent();
    45     mContext->gl->fDeleteTextures(1, &mGLName);
    46     LinkedListElement<WebGLTexture>::removeFrom(mContext->mTextures);
    47 }
    49 int64_t
    50 WebGLTexture::ImageInfo::MemoryUsage() const {
    51     if (mImageDataStatus == WebGLImageDataStatus::NoImageData)
    52         return 0;
    53     int64_t bitsPerTexel = WebGLContext::GetBitsPerTexel(mWebGLFormat, mWebGLType);
    54     return int64_t(mWidth) * int64_t(mHeight) * bitsPerTexel/8;
    55 }
    57 int64_t
    58 WebGLTexture::MemoryUsage() const {
    59     if (IsDeleted())
    60         return 0;
    61     int64_t result = 0;
    62     for(size_t face = 0; face < mFacesCount; face++) {
    63         if (mHaveGeneratedMipmap) {
    64             // Each mipmap level is 1/4 the size of the previous level
    65             // 1 + x + x^2 + ... = 1/(1-x)
    66             // for x = 1/4, we get 1/(1-1/4) = 4/3
    67             result += ImageInfoAtFace(face, 0).MemoryUsage() * 4 / 3;
    68         } else {
    69             for(size_t level = 0; level <= mMaxLevelWithCustomImages; level++)
    70                 result += ImageInfoAtFace(face, level).MemoryUsage();
    71         }
    72     }
    73     return result;
    74 }
    76 bool
    77 WebGLTexture::DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(GLenum texImageTarget) const {
    78     if (mHaveGeneratedMipmap)
    79         return true;
    81     // We want a copy here so we can modify it temporarily.
    82     ImageInfo expected = ImageInfoAt(texImageTarget, 0);
    84     // checks if custom level>0 images are all defined up to the highest level defined
    85     // and have the expected dimensions
    86     for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) {
    87         const ImageInfo& actual = ImageInfoAt(texImageTarget, level);
    88         if (actual != expected)
    89             return false;
    90         expected.mWidth = std::max(1, expected.mWidth >> 1);
    91         expected.mHeight = std::max(1, expected.mHeight >> 1);
    93         // if the current level has size 1x1, we can stop here: the spec doesn't seem to forbid the existence
    94         // of extra useless levels.
    95         if (actual.mWidth == 1 && actual.mHeight == 1)
    96             return true;
    97     }
    99     // if we're here, we've exhausted all levels without finding a 1x1 image
   100     return false;
   101 }
   103 void
   104 WebGLTexture::Bind(GLenum aTarget) {
   105     // this function should only be called by bindTexture().
   106     // it assumes that the GL context is already current.
   108     bool firstTimeThisTextureIsBound = !mHasEverBeenBound;
   110     if (!firstTimeThisTextureIsBound && aTarget != mTarget) {
   111         mContext->ErrorInvalidOperation("bindTexture: this texture has already been bound to a different target");
   112         // very important to return here before modifying texture state! This was the place when I lost a whole day figuring
   113         // very strange 'invalid write' crashes.
   114         return;
   115     }
   117     mTarget = aTarget;
   119     mContext->gl->fBindTexture(mTarget, mGLName);
   121     if (firstTimeThisTextureIsBound) {
   122         mFacesCount = (mTarget == LOCAL_GL_TEXTURE_2D) ? 1 : 6;
   123         EnsureMaxLevelWithCustomImagesAtLeast(0);
   124         SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown);
   126         // thanks to the WebKit people for finding this out: GL_TEXTURE_WRAP_R is not
   127         // present in GLES 2, but is present in GL and it seems as if for cube maps
   128         // we need to set it to GL_CLAMP_TO_EDGE to get the expected GLES behavior.
   129         if (mTarget == LOCAL_GL_TEXTURE_CUBE_MAP && !mContext->gl->IsGLES())
   130             mContext->gl->fTexParameteri(mTarget, LOCAL_GL_TEXTURE_WRAP_R, LOCAL_GL_CLAMP_TO_EDGE);
   131     }
   133     mHasEverBeenBound = true;
   134 }
   136 void
   137 WebGLTexture::SetImageInfo(GLenum aTarget, GLint aLevel,
   138                   GLsizei aWidth, GLsizei aHeight,
   139                   GLenum aFormat, GLenum aType, WebGLImageDataStatus aStatus)
   140 {
   141     if ( (aTarget == LOCAL_GL_TEXTURE_2D) != (mTarget == LOCAL_GL_TEXTURE_2D) )
   142         return;
   144     EnsureMaxLevelWithCustomImagesAtLeast(aLevel);
   146     ImageInfoAt(aTarget, aLevel) = ImageInfo(aWidth, aHeight, aFormat, aType, aStatus);
   148     if (aLevel > 0)
   149         SetCustomMipmap();
   151     // Invalidate framebuffer status cache
   152     NotifyFBsStatusChanged();
   154     SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown);
   155 }
   157 void
   158 WebGLTexture::SetGeneratedMipmap() {
   159     if (!mHaveGeneratedMipmap) {
   160         mHaveGeneratedMipmap = true;
   161         SetFakeBlackStatus(WebGLTextureFakeBlackStatus::Unknown);
   162     }
   163 }
   165 void
   166 WebGLTexture::SetCustomMipmap() {
   167     if (mHaveGeneratedMipmap) {
   168         // if we were in GeneratedMipmap mode and are now switching to CustomMipmap mode,
   169         // we need to compute now all the mipmap image info.
   171         // since we were in GeneratedMipmap mode, we know that the level 0 images all have the same info,
   172         // and are power-of-two.
   173         ImageInfo imageInfo = ImageInfoAtFace(0, 0);
   174         NS_ASSERTION(imageInfo.IsPowerOfTwo(), "this texture is NPOT, so how could GenerateMipmap() ever accept it?");
   176         GLsizei size = std::max(imageInfo.mWidth, imageInfo.mHeight);
   178         // so, the size is a power of two, let's find its log in base 2.
   179         size_t maxLevel = 0;
   180         for (GLsizei n = size; n > 1; n >>= 1)
   181             ++maxLevel;
   183         EnsureMaxLevelWithCustomImagesAtLeast(maxLevel);
   185         for (size_t level = 1; level <= maxLevel; ++level) {
   186             // again, since the sizes are powers of two, no need for any max(1,x) computation
   187             imageInfo.mWidth >>= 1;
   188             imageInfo.mHeight >>= 1;
   189             for(size_t face = 0; face < mFacesCount; ++face)
   190                 ImageInfoAtFace(face, level) = imageInfo;
   191         }
   192     }
   193     mHaveGeneratedMipmap = false;
   194 }
   196 bool
   197 WebGLTexture::AreAllLevel0ImageInfosEqual() const {
   198     for (size_t face = 1; face < mFacesCount; ++face) {
   199         if (ImageInfoAtFace(face, 0) != ImageInfoAtFace(0, 0))
   200             return false;
   201     }
   202     return true;
   203 }
   205 bool
   206 WebGLTexture::IsMipmapTexture2DComplete() const {
   207     if (mTarget != LOCAL_GL_TEXTURE_2D)
   208         return false;
   209     if (!ImageInfoAt(LOCAL_GL_TEXTURE_2D, 0).IsPositive())
   210         return false;
   211     if (mHaveGeneratedMipmap)
   212         return true;
   213     return DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(LOCAL_GL_TEXTURE_2D);
   214 }
   216 bool
   217 WebGLTexture::IsCubeComplete() const {
   218     if (mTarget != LOCAL_GL_TEXTURE_CUBE_MAP)
   219         return false;
   220     const ImageInfo &first = ImageInfoAt(LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0);
   221     if (!first.IsPositive() || !first.IsSquare())
   222         return false;
   223     return AreAllLevel0ImageInfosEqual();
   224 }
   226 static GLenum
   227 GLCubeMapFaceById(int id)
   228 {
   229     GLenum result = LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + id;
   230     MOZ_ASSERT(result >= LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X &&
   231                result <= LOCAL_GL_TEXTURE_CUBE_MAP_NEGATIVE_Z);
   232     return result;
   233 }
   235 bool
   236 WebGLTexture::IsMipmapCubeComplete() const {
   237     if (!IsCubeComplete()) // in particular, this checks that this is a cube map
   238         return false;
   239     for (int i = 0; i < 6; i++) {
   240         GLenum face = GLCubeMapFaceById(i);
   241         if (!DoesTexture2DMipmapHaveAllLevelsConsistentlyDefined(face))
   242             return false;
   243     }
   244     return true;
   245 }
   247 WebGLTextureFakeBlackStatus
   248 WebGLTexture::ResolvedFakeBlackStatus() {
   249     if (MOZ_LIKELY(mFakeBlackStatus != WebGLTextureFakeBlackStatus::Unknown)) {
   250         return mFakeBlackStatus;
   251     }
   253     // Determine if the texture needs to be faked as a black texture.
   254     // See 3.8.2 Shader Execution in the OpenGL ES 2.0.24 spec.
   256     for (size_t face = 0; face < mFacesCount; ++face) {
   257         if (ImageInfoAtFace(face, 0).mImageDataStatus == WebGLImageDataStatus::NoImageData) {
   258             // In case of undefined texture image, we don't print any message because this is a very common
   259             // and often legitimate case (asynchronous texture loading).
   260             mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   261             return mFakeBlackStatus;
   262         }
   263     }
   265     const char *msg_rendering_as_black
   266         = "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, "
   267           "because it";
   269     if (mTarget == LOCAL_GL_TEXTURE_2D)
   270     {
   271         if (DoesMinFilterRequireMipmap())
   272         {
   273             if (!IsMipmapTexture2DComplete()) {
   274                 mContext->GenerateWarning
   275                     ("%s is a 2D texture, with a minification filter requiring a mipmap, "
   276                       "and is not mipmap complete (as defined in section 3.7.10).", msg_rendering_as_black);
   277                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   278             } else if (!ImageInfoAt(mTarget, 0).IsPowerOfTwo()) {
   279                 mContext->GenerateWarning
   280                     ("%s is a 2D texture, with a minification filter requiring a mipmap, "
   281                       "and either its width or height is not a power of two.", msg_rendering_as_black);
   282                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   283             }
   284         }
   285         else // no mipmap required
   286         {
   287             if (!ImageInfoAt(mTarget, 0).IsPositive()) {
   288                 mContext->GenerateWarning
   289                     ("%s is a 2D texture and its width or height is equal to zero.",
   290                       msg_rendering_as_black);
   291                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   292             } else if (!AreBothWrapModesClampToEdge() && !ImageInfoAt(mTarget, 0).IsPowerOfTwo()) {
   293                 mContext->GenerateWarning
   294                     ("%s is a 2D texture, with a minification filter not requiring a mipmap, "
   295                       "with its width or height not a power of two, and with a wrap mode "
   296                       "different from CLAMP_TO_EDGE.", msg_rendering_as_black);
   297                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   298             }
   299         }
   300     }
   301     else // cube map
   302     {
   303         bool areAllLevel0ImagesPOT = true;
   304         for (size_t face = 0; face < mFacesCount; ++face)
   305             areAllLevel0ImagesPOT &= ImageInfoAtFace(face, 0).IsPowerOfTwo();
   307         if (DoesMinFilterRequireMipmap())
   308         {
   309             if (!IsMipmapCubeComplete()) {
   310                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, "
   311                             "and is not mipmap cube complete (as defined in section 3.7.10).",
   312                             msg_rendering_as_black);
   313                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   314             } else if (!areAllLevel0ImagesPOT) {
   315                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter requiring a mipmap, "
   316                             "and either the width or the height of some level 0 image is not a power of two.",
   317                             msg_rendering_as_black);
   318                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   319             }
   320         }
   321         else // no mipmap required
   322         {
   323             if (!IsCubeComplete()) {
   324                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, "
   325                             "and is not cube complete (as defined in section 3.7.10).",
   326                             msg_rendering_as_black);
   327                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   328             } else if (!AreBothWrapModesClampToEdge() && !areAllLevel0ImagesPOT) {
   329                 mContext->GenerateWarning("%s is a cube map texture, with a minification filter not requiring a mipmap, "
   330                             "with some level 0 image having width or height not a power of two, and with a wrap mode "
   331                             "different from CLAMP_TO_EDGE.", msg_rendering_as_black);
   332                 mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   333             }
   334         }
   335     }
   337     if (ImageInfoBase().mWebGLType == LOCAL_GL_FLOAT &&
   338         !Context()->IsExtensionEnabled(WebGLExtensionID::OES_texture_float_linear))
   339     {
   340         if (mMinFilter == LOCAL_GL_LINEAR ||
   341             mMinFilter == LOCAL_GL_LINEAR_MIPMAP_LINEAR ||
   342             mMinFilter == LOCAL_GL_LINEAR_MIPMAP_NEAREST ||
   343             mMinFilter == LOCAL_GL_NEAREST_MIPMAP_LINEAR)
   344         {
   345             mContext->GenerateWarning("%s is a texture with a linear minification filter, "
   346                                       "which is not compatible with gl.FLOAT by default. "
   347                                       "Try enabling the OES_texture_float_linear extension if supported.", msg_rendering_as_black);
   348             mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   349         }
   350         else if (mMagFilter == LOCAL_GL_LINEAR)
   351         {
   352             mContext->GenerateWarning("%s is a texture with a linear magnification filter, "
   353                                       "which is not compatible with gl.FLOAT by default. "
   354                                       "Try enabling the OES_texture_float_linear extension if supported.", msg_rendering_as_black);
   355             mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   356         }
   357     } else if (ImageInfoBase().mWebGLType == LOCAL_GL_HALF_FLOAT_OES &&
   358                !Context()->IsExtensionEnabled(WebGLExtensionID::OES_texture_half_float_linear))
   359     {
   360         if (mMinFilter == LOCAL_GL_LINEAR ||
   361             mMinFilter == LOCAL_GL_LINEAR_MIPMAP_LINEAR ||
   362             mMinFilter == LOCAL_GL_LINEAR_MIPMAP_NEAREST ||
   363             mMinFilter == LOCAL_GL_NEAREST_MIPMAP_LINEAR)
   364         {
   365             mContext->GenerateWarning("%s is a texture with a linear minification filter, "
   366                                       "which is not compatible with gl.HALF_FLOAT by default. "
   367                                       "Try enabling the OES_texture_half_float_linear extension if supported.", msg_rendering_as_black);
   368             mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   369         }
   370         else if (mMagFilter == LOCAL_GL_LINEAR)
   371         {
   372             mContext->GenerateWarning("%s is a texture with a linear magnification filter, "
   373                                       "which is not compatible with gl.HALF_FLOAT by default. "
   374                                       "Try enabling the OES_texture_half_float_linear extension if supported.", msg_rendering_as_black);
   375             mFakeBlackStatus = WebGLTextureFakeBlackStatus::IncompleteTexture;
   376         }
   377     }
   379     // We have exhausted all cases of incomplete textures, where we would need opaque black.
   380     // We may still need transparent black in case of uninitialized image data.
   381     bool hasUninitializedImageData = false;
   382     for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) {
   383         for (size_t face = 0; face < mFacesCount; ++face) {
   384             hasUninitializedImageData |= (ImageInfoAtFace(face, level).mImageDataStatus == WebGLImageDataStatus::UninitializedImageData);
   385         }
   386     }
   388     if (hasUninitializedImageData) {
   389         bool hasAnyInitializedImageData = false;
   390         for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) {
   391             for (size_t face = 0; face < mFacesCount; ++face) {
   392                 if (ImageInfoAtFace(face, level).mImageDataStatus == WebGLImageDataStatus::InitializedImageData) {
   393                     hasAnyInitializedImageData = true;
   394                     break;
   395                 }
   396             }
   397             if (hasAnyInitializedImageData) {
   398                 break;
   399             }
   400         }
   402         if (hasAnyInitializedImageData) {
   403             // The texture contains some initialized image data, and some uninitialized image data.
   404             // In this case, we have no choice but to initialize all image data now. Fortunately,
   405             // in this case we know that we can't be dealing with a depth texture per WEBGL_depth_texture
   406             // and ANGLE_depth_texture (which allow only one image per texture) so we can assume that
   407             // glTexImage2D is able to upload data to images.
   408             for (size_t level = 0; level <= mMaxLevelWithCustomImages; ++level) {
   409                 for (size_t face = 0; face < mFacesCount; ++face) {
   410                     GLenum imageTarget = mTarget == LOCAL_GL_TEXTURE_2D
   411                                          ? LOCAL_GL_TEXTURE_2D
   412                                          : LOCAL_GL_TEXTURE_CUBE_MAP_POSITIVE_X + face;
   413                     const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level);
   414                     if (imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData) {
   415                         DoDeferredImageInitialization(imageTarget, level);
   416                     }
   417                 }
   418             }
   419             mFakeBlackStatus = WebGLTextureFakeBlackStatus::NotNeeded;
   420         } else {
   421             // The texture only contains uninitialized image data. In this case,
   422             // we can use a black texture for it.
   423             mFakeBlackStatus = WebGLTextureFakeBlackStatus::UninitializedImageData;
   424         }
   425     }
   427     // we have exhausted all cases where we do need fakeblack, so if the status is still unknown,
   428     // that means that we do NOT need it.
   429     if (mFakeBlackStatus == WebGLTextureFakeBlackStatus::Unknown) {
   430         mFakeBlackStatus = WebGLTextureFakeBlackStatus::NotNeeded;
   431     }
   433     MOZ_ASSERT(mFakeBlackStatus != WebGLTextureFakeBlackStatus::Unknown);
   434     return mFakeBlackStatus;
   435 }
   437 void
   438 WebGLTexture::DoDeferredImageInitialization(GLenum imageTarget, GLint level)
   439 {
   440     const ImageInfo& imageInfo = ImageInfoAt(imageTarget, level);
   441     MOZ_ASSERT(imageInfo.mImageDataStatus == WebGLImageDataStatus::UninitializedImageData);
   443     mContext->MakeContextCurrent();
   444     gl::ScopedBindTexture autoBindTex(mContext->gl, GLName(), mTarget);
   446     GLenum format = imageInfo.mWebGLFormat;
   447     GLenum type = imageInfo.mWebGLType;
   448     WebGLTexelFormat texelformat = GetWebGLTexelFormat(format, type);
   449     uint32_t texelsize = WebGLTexelConversions::TexelBytesForFormat(texelformat);
   450     CheckedUint32 checked_byteLength
   451         = WebGLContext::GetImageSize(
   452                         imageInfo.mHeight,
   453                         imageInfo.mWidth,
   454                         texelsize,
   455                         mContext->mPixelStoreUnpackAlignment);
   456     MOZ_ASSERT(checked_byteLength.isValid()); // should have been checked earlier
   457     void *zeros = calloc(1, checked_byteLength.value());
   459     gl::GLContext* gl = mContext->gl;
   460     GLenum driverType = DriverTypeFromType(gl, type);
   461     GLenum driverInternalFormat = LOCAL_GL_NONE;
   462     GLenum driverFormat = LOCAL_GL_NONE;
   463     DriverFormatsFromFormatAndType(gl, format, type, &driverInternalFormat, &driverFormat);
   465     mContext->GetAndFlushUnderlyingGLErrors();
   466     gl->fTexImage2D(imageTarget, level, driverInternalFormat,
   467                     imageInfo.mWidth, imageInfo.mHeight,
   468                     0, driverFormat, driverType,
   469                     zeros);
   470     GLenum error = mContext->GetAndFlushUnderlyingGLErrors();
   472     free(zeros);
   473     SetImageDataStatus(imageTarget, level, WebGLImageDataStatus::InitializedImageData);
   475     if (error) {
   476       // Should only be OUT_OF_MEMORY. Anyway, there's no good way to recover from this here.
   477       MOZ_CRASH(); // errors on texture upload have been related to video memory exposure in the past.
   478       return;
   479     }
   480 }
   482 NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(WebGLTexture)
   484 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(WebGLTexture, AddRef)
   485 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(WebGLTexture, Release)

mercurial