Bug 916643 - Refactory ImageEncoder. r=roc
authorAlfredo Yang <ayang@mozilla.com>
Thu, 04 Sep 2014 02:08:00 +0200
changeset 203616 bb3377f01ad9b21f1744f743d8329df452a506a5
parent 203615 c622525937b16fffc876f08e7a53807e45242655
child 203617 d017e24a1ceae3da2962c958b02e4b855955178b
push id1
push userroot
push dateMon, 20 Oct 2014 17:29:22 +0000
reviewersroc
bugs916643
milestone35.0a1
Bug 916643 - Refactory ImageEncoder. r=roc
content/html/content/src/HTMLCanvasElement.cpp
dom/base/ImageEncoder.cpp
dom/base/ImageEncoder.h
dom/base/moz.build
dom/canvas/ImageEncoder.cpp
dom/canvas/ImageEncoder.h
dom/canvas/moz.build
--- a/content/html/content/src/HTMLCanvasElement.cpp
+++ b/content/html/content/src/HTMLCanvasElement.cpp
@@ -536,27 +536,59 @@ HTMLCanvasElement::ToBlob(JSContext* aCx
 #endif
 
   uint8_t* imageBuffer = nullptr;
   int32_t format = 0;
   if (mCurrentContext) {
     mCurrentContext->GetImageBuffer(&imageBuffer, &format);
   }
 
+  // Encoder callback when encoding is complete.
+  class EncodeCallback : public EncodeCompleteCallback
+  {
+  public:
+    EncodeCallback(nsIGlobalObject* aGlobal, FileCallback* aCallback)
+      : mGlobal(aGlobal)
+      , mFileCallback(aCallback) {}
+
+    // This is called on main thread.
+    nsresult ReceiveBlob(already_AddRefed<DOMFile> aBlob)
+    {
+      nsRefPtr<DOMFile> blob = aBlob;
+      uint64_t size;
+      nsresult rv = blob->GetSize(&size);
+      if (NS_SUCCEEDED(rv)) {
+        AutoJSAPI jsapi;
+        jsapi.Init(mGlobal);
+        JS_updateMallocCounter(jsapi.cx(), size);
+      }
+
+      mozilla::ErrorResult error;
+      mFileCallback->Call(blob, error);
+
+      mGlobal = nullptr;
+      mFileCallback = nullptr;
+
+      return error.ErrorCode();
+    }
+
+    nsCOMPtr<nsIGlobalObject> mGlobal;
+    nsRefPtr<FileCallback> mFileCallback;
+  };
+
   nsCOMPtr<nsIGlobalObject> global = OwnerDoc()->GetScopeObject();
   MOZ_ASSERT(global);
+  nsRefPtr<EncodeCompleteCallback> callback = new EncodeCallback(global, &aCallback);
   aRv = ImageEncoder::ExtractDataAsync(type,
                                        params,
                                        usingCustomParseOptions,
                                        imageBuffer,
                                        format,
                                        GetSize(),
-                                       mCurrentContext,
-                                       global,
-                                       aCallback);
+                                       callback);
 }
 
 already_AddRefed<nsIDOMFile>
 HTMLCanvasElement::MozGetAsFile(const nsAString& aName,
                                 const nsAString& aType,
                                 ErrorResult& aRv)
 {
   nsCOMPtr<nsIDOMFile> file;
rename from dom/canvas/ImageEncoder.cpp
rename to dom/base/ImageEncoder.cpp
--- a/dom/canvas/ImageEncoder.cpp
+++ b/dom/base/ImageEncoder.cpp
@@ -3,69 +3,106 @@
  * 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 "ImageEncoder.h"
 #include "mozilla/dom/CanvasRenderingContext2D.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/DataSurfaceHelpers.h"
 #include "mozilla/RefPtr.h"
+#include "mozilla/SyncRunnable.h"
+#include "gfxUtils.h"
 
 using namespace mozilla::gfx;
 
 namespace mozilla {
 namespace dom {
 
+// This class should be placed inside GetBRGADataSourceSurfaceSync(). However,
+// due to B2G ICS uses old complier (C++98/03) which forbids local class as
+// template parameter, we need to move this class outside.
+class SurfaceHelper : public nsRunnable {
+public:
+  SurfaceHelper(TemporaryRef<layers::Image> aImage) : mImage(aImage) {}
+
+  // It retrieves a SourceSurface reference and convert color format on main
+  // thread and passes DataSourceSurface to caller thread.
+  NS_IMETHOD Run() {
+    // It guarantees the reference will be released on main thread.
+    nsCountedRef<nsMainThreadSourceSurfaceRef> surface;
+    surface.own(mImage->GetAsSourceSurface().drop());
+
+    if (surface->GetFormat() == gfx::SurfaceFormat::B8G8R8A8) {
+      mDataSourceSurface = surface->GetDataSurface();
+    } else {
+      mDataSourceSurface = gfxUtils::
+        CopySurfaceToDataSourceSurfaceWithFormat(surface,
+                                                 gfx::SurfaceFormat::B8G8R8A8);
+    }
+    return NS_OK;
+  }
+
+  TemporaryRef<gfx::DataSourceSurface> GetDataSurfaceSafe() {
+    nsCOMPtr<nsIThread> mainThread = do_GetMainThread();
+    MOZ_ASSERT(mainThread);
+    SyncRunnable::DispatchToThread(mainThread, this, false);
+
+    return mDataSourceSurface.forget();
+  }
+
+private:
+  RefPtr<layers::Image> mImage;
+  RefPtr<gfx::DataSourceSurface> mDataSourceSurface;
+};
+
+// This function returns a DataSourceSurface in B8G8R8A8 format.
+// It uses SourceSurface to do format convert. Because most SourceSurface in
+// image formats should be referenced or dereferenced on main thread, it uses a
+// sync class SurfaceHelper to retrieve SourceSurface and convert to B8G8R8A8 on
+// main thread.
+TemporaryRef<DataSourceSurface>
+GetBRGADataSourceSurfaceSync(TemporaryRef<layers::Image> aImage)
+{
+  nsRefPtr<SurfaceHelper> helper = new SurfaceHelper(aImage);
+  return helper->GetDataSurfaceSafe();
+}
+
 class EncodingCompleteEvent : public nsRunnable
 {
   virtual ~EncodingCompleteEvent() {}
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
-  EncodingCompleteEvent(nsIGlobalObject* aGlobal,
-                        nsIThread* aEncoderThread,
-                        FileCallback& aCallback)
+  EncodingCompleteEvent(nsIThread* aEncoderThread,
+                        EncodeCompleteCallback* aEncodeCompleteCallback)
     : mImgSize(0)
     , mType()
     , mImgData(nullptr)
-    , mGlobal(aGlobal)
     , mEncoderThread(aEncoderThread)
-    , mCallback(&aCallback)
+    , mEncodeCompleteCallback(aEncodeCompleteCallback)
     , mFailed(false)
   {}
 
   NS_IMETHOD Run()
   {
+    nsresult rv = NS_OK;
     MOZ_ASSERT(NS_IsMainThread());
 
-    mozilla::ErrorResult rv;
-
     if (!mFailed) {
       nsRefPtr<DOMFile> blob =
         DOMFile::CreateMemoryFile(mImgData, mImgSize, mType);
 
-      {
-        AutoJSAPI jsapi;
-        jsapi.Init(mGlobal);
-        JS_updateMallocCounter(jsapi.cx(), mImgSize);
-      }
-
-      mCallback->Call(blob, rv);
+      rv = mEncodeCompleteCallback->ReceiveBlob(blob.forget());
     }
 
-    // These members aren't thread-safe. We're making sure that they're being
-    // released on the main thread here. Otherwise, they could be getting
-    // released by EncodingRunnable's destructor on the encoding thread
-    // (bug 916128).
-    mGlobal = nullptr;
-    mCallback = nullptr;
+    mEncodeCompleteCallback = nullptr;
 
     mEncoderThread->Shutdown();
-    return rv.ErrorCode();
+    return rv;
   }
 
   void SetMembers(void* aImgData, uint64_t aImgSize, const nsAutoString& aType)
   {
     mImgData = aImgData;
     mImgSize = aImgSize;
     mType = aType;
   }
@@ -74,69 +111,72 @@ public:
   {
     mFailed = true;
   }
 
 private:
   uint64_t mImgSize;
   nsAutoString mType;
   void* mImgData;
-  nsCOMPtr<nsIGlobalObject> mGlobal;
   nsCOMPtr<nsIThread> mEncoderThread;
-  nsRefPtr<FileCallback> mCallback;
+  nsRefPtr<EncodeCompleteCallback> mEncodeCompleteCallback;
   bool mFailed;
 };
 
 NS_IMPL_ISUPPORTS_INHERITED0(EncodingCompleteEvent, nsRunnable);
 
 class EncodingRunnable : public nsRunnable
 {
   virtual ~EncodingRunnable() {}
 
 public:
   NS_DECL_ISUPPORTS_INHERITED
 
   EncodingRunnable(const nsAString& aType,
                    const nsAString& aOptions,
                    uint8_t* aImageBuffer,
+                   layers::Image* aImage,
                    imgIEncoder* aEncoder,
                    EncodingCompleteEvent* aEncodingCompleteEvent,
                    int32_t aFormat,
                    const nsIntSize aSize,
                    bool aUsingCustomOptions)
     : mType(aType)
     , mOptions(aOptions)
     , mImageBuffer(aImageBuffer)
+    , mImage(aImage)
     , mEncoder(aEncoder)
     , mEncodingCompleteEvent(aEncodingCompleteEvent)
     , mFormat(aFormat)
     , mSize(aSize)
     , mUsingCustomOptions(aUsingCustomOptions)
   {}
 
   nsresult ProcessImageData(uint64_t* aImgSize, void** aImgData)
   {
     nsCOMPtr<nsIInputStream> stream;
     nsresult rv = ImageEncoder::ExtractDataInternal(mType,
                                                     mOptions,
                                                     mImageBuffer,
                                                     mFormat,
                                                     mSize,
+                                                    mImage,
                                                     nullptr,
                                                     getter_AddRefs(stream),
                                                     mEncoder);
 
     // If there are unrecognized custom parse options, we should fall back to
     // the default values for the encoder without any options at all.
     if (rv == NS_ERROR_INVALID_ARG && mUsingCustomOptions) {
       rv = ImageEncoder::ExtractDataInternal(mType,
                                              EmptyString(),
                                              mImageBuffer,
                                              mFormat,
                                              mSize,
+                                             mImage,
                                              nullptr,
                                              getter_AddRefs(stream),
                                              mEncoder);
     }
     NS_ENSURE_SUCCESS(rv, rv);
 
     rv = stream->Available(aImgSize);
     NS_ENSURE_SUCCESS(rv, rv);
@@ -168,16 +208,17 @@ public:
 
     return rv;
   }
 
 private:
   nsAutoString mType;
   nsAutoString mOptions;
   nsAutoArrayPtr<uint8_t> mImageBuffer;
+  nsRefPtr<layers::Image> mImage;
   nsCOMPtr<imgIEncoder> mEncoder;
   nsRefPtr<EncodingCompleteEvent> mEncodingCompleteEvent;
   int32_t mFormat;
   const nsIntSize mSize;
   bool mUsingCustomOptions;
 };
 
 NS_IMPL_ISUPPORTS_INHERITED0(EncodingRunnable, nsRunnable);
@@ -190,49 +231,80 @@ ImageEncoder::ExtractData(nsAString& aTy
                           nsICanvasRenderingContextInternal* aContext,
                           nsIInputStream** aStream)
 {
   nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
   if (!encoder) {
     return NS_IMAGELIB_ERROR_NO_ENCODER;
   }
 
-  return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize, aContext,
-                             aStream, encoder);
+  return ExtractDataInternal(aType, aOptions, nullptr, 0, aSize, nullptr,
+                             aContext, aStream, encoder);
+}
+
+
+/* static */
+nsresult
+ImageEncoder::ExtractDataFromLayersImageAsync(nsAString& aType,
+                                              const nsAString& aOptions,
+                                              bool aUsingCustomOptions,
+                                              layers::Image* aImage,
+                                              EncodeCompleteCallback* aEncodeCallback)
+{
+  nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
+  if (!encoder) {
+    return NS_IMAGELIB_ERROR_NO_ENCODER;
+  }
+
+  nsCOMPtr<nsIThread> encoderThread;
+  nsresult rv = NS_NewThread(getter_AddRefs(encoderThread), nullptr);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsRefPtr<EncodingCompleteEvent> completeEvent =
+    new EncodingCompleteEvent(encoderThread, aEncodeCallback);
+
+  nsIntSize size(aImage->GetSize().width, aImage->GetSize().height);
+  nsCOMPtr<nsIRunnable> event = new EncodingRunnable(aType,
+                                                     aOptions,
+                                                     nullptr,
+                                                     aImage,
+                                                     encoder,
+                                                     completeEvent,
+                                                     imgIEncoder::INPUT_FORMAT_HOSTARGB,
+                                                     size,
+                                                     aUsingCustomOptions);
+  return encoderThread->Dispatch(event, NS_DISPATCH_NORMAL);
 }
 
 /* static */
 nsresult
 ImageEncoder::ExtractDataAsync(nsAString& aType,
                                const nsAString& aOptions,
                                bool aUsingCustomOptions,
                                uint8_t* aImageBuffer,
                                int32_t aFormat,
                                const nsIntSize aSize,
-                               nsICanvasRenderingContextInternal* aContext,
-                               nsIGlobalObject* aGlobal,
-                               FileCallback& aCallback)
+                               EncodeCompleteCallback* aEncodeCallback)
 {
-  MOZ_ASSERT(aGlobal);
-
   nsCOMPtr<imgIEncoder> encoder = ImageEncoder::GetImageEncoder(aType);
   if (!encoder) {
     return NS_IMAGELIB_ERROR_NO_ENCODER;
   }
 
   nsCOMPtr<nsIThread> encoderThread;
   nsresult rv = NS_NewThread(getter_AddRefs(encoderThread), nullptr);
   NS_ENSURE_SUCCESS(rv, rv);
 
   nsRefPtr<EncodingCompleteEvent> completeEvent =
-    new EncodingCompleteEvent(aGlobal, encoderThread, aCallback);
+    new EncodingCompleteEvent(encoderThread, aEncodeCallback);
 
   nsCOMPtr<nsIRunnable> event = new EncodingRunnable(aType,
                                                      aOptions,
                                                      aImageBuffer,
+                                                     nullptr,
                                                      encoder,
                                                      completeEvent,
                                                      aFormat,
                                                      aSize,
                                                      aUsingCustomOptions);
   return encoderThread->Dispatch(event, NS_DISPATCH_NORMAL);
 }
 
@@ -257,16 +329,17 @@ ImageEncoder::GetInputStream(int32_t aWi
 
 /* static */
 nsresult
 ImageEncoder::ExtractDataInternal(const nsAString& aType,
                                   const nsAString& aOptions,
                                   uint8_t* aImageBuffer,
                                   int32_t aFormat,
                                   const nsIntSize aSize,
+                                  layers::Image* aImage,
                                   nsICanvasRenderingContextInternal* aContext,
                                   nsIInputStream** aStream,
                                   imgIEncoder* aEncoder)
 {
   if (aSize.IsEmpty()) {
     return NS_ERROR_INVALID_ARG;
   }
 
@@ -283,16 +356,63 @@ ImageEncoder::ExtractDataInternal(const 
       aEncoder,
       nsPromiseFlatString(aOptions).get(),
       getter_AddRefs(imgStream));
   } else if (aContext) {
     NS_ConvertUTF16toUTF8 encoderType(aType);
     rv = aContext->GetInputStream(encoderType.get(),
                                   nsPromiseFlatString(aOptions).get(),
                                   getter_AddRefs(imgStream));
+  } else if (aImage) {
+    // It is safe to convert PlanarYCbCr format from YUV to RGB off-main-thread.
+    // Other image formats could have problem to convert format off-main-thread.
+    // So here it uses a help function GetBRGADataSourceSurfaceSync() to convert
+    // format on main thread.
+    if (aImage->GetFormat() == ImageFormat::PLANAR_YCBCR) {
+      nsTArray<uint8_t> data;
+      layers::PlanarYCbCrImage* ycbcrImage = static_cast<layers::PlanarYCbCrImage*> (aImage);
+      gfxImageFormat format = gfxImageFormat::ARGB32;
+      uint32_t stride = GetAlignedStride<16>(aSize.width * 4);
+      size_t length = BufferSizeFromStrideAndHeight(stride, aSize.height);
+      data.SetCapacity(length);
+
+      gfxUtils::ConvertYCbCrToRGB(*ycbcrImage->GetData(),
+                                  format,
+                                  aSize,
+                                  data.Elements(),
+                                  stride);
+
+      rv = aEncoder->InitFromData(data.Elements(),
+                                  aSize.width * aSize.height * 4,
+                                  aSize.width,
+                                  aSize.height,
+                                  aSize.width * 4,
+                                  imgIEncoder::INPUT_FORMAT_HOSTARGB,
+                                  aOptions);
+    } else {
+      RefPtr<gfx::DataSourceSurface> dataSurface;
+      dataSurface = GetBRGADataSourceSurfaceSync(aImage);
+
+      DataSourceSurface::MappedSurface map;
+      if (!dataSurface->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
+        return NS_ERROR_INVALID_ARG;
+      }
+      rv = aEncoder->InitFromData(map.mData,
+                                  aSize.width * aSize.height * 4,
+                                  aSize.width,
+                                  aSize.height,
+                                  aSize.width * 4,
+                                  imgIEncoder::INPUT_FORMAT_HOSTARGB,
+                                  aOptions);
+      dataSurface->Unmap();
+    }
+
+    if (NS_SUCCEEDED(rv)) {
+      imgStream = do_QueryInterface(aEncoder);
+    }
   } else {
     // no context, so we have to encode an empty image
     // note that if we didn't have a current context, the spec says we're
     // supposed to just return transparent black pixels of the canvas
     // dimensions.
     RefPtr<DataSourceSurface> emptyCanvas =
       Factory::CreateDataSourceSurfaceWithStride(IntSize(aSize.width, aSize.height),
                                                  SurfaceFormat::B8G8R8A8,
rename from dom/canvas/ImageEncoder.h
rename to dom/base/ImageEncoder.h
--- a/dom/canvas/ImageEncoder.h
+++ b/dom/base/ImageEncoder.h
@@ -10,21 +10,26 @@
 #include "nsDOMFile.h"
 #include "nsError.h"
 #include "mozilla/dom/HTMLCanvasElementBinding.h"
 #include "nsLayoutUtils.h"
 #include "nsNetUtil.h"
 #include "nsSize.h"
 
 class nsICanvasRenderingContextInternal;
-class nsIGlobalObject;
 
 namespace mozilla {
+
+namespace layers {
+class Image;
+}
+
 namespace dom {
 
+class EncodeCompleteCallback;
 class EncodingRunnable;
 
 class ImageEncoder
 {
 public:
   // Extracts data synchronously and gives you a stream containing the image
   // represented by aContext. aType may change to "image/png" if we had to fall
   // back to a PNG encoder. A return value of NS_OK implies successful data
@@ -39,25 +44,34 @@ public:
 
   // Extracts data asynchronously. aType may change to "image/png" if we had to
   // fall back to a PNG encoder. aOptions are the options to be passed to the
   // encoder and aUsingCustomOptions specifies whether custom parse options were
   // used (i.e. by using -moz-parse-options). If there are any unrecognized
   // custom parse options, we fall back to the default values for the encoder
   // without any options at all. A return value of NS_OK only implies
   // successful dispatching of the extraction step to the encoding thread.
+  // aEncodeCallback will be called on main thread when encoding process is
+  // success.
   static nsresult ExtractDataAsync(nsAString& aType,
                                    const nsAString& aOptions,
                                    bool aUsingCustomOptions,
                                    uint8_t* aImageBuffer,
                                    int32_t aFormat,
                                    const nsIntSize aSize,
-                                   nsICanvasRenderingContextInternal* aContext,
-                                   nsIGlobalObject* aGlobal,
-                                   FileCallback& aCallback);
+                                   EncodeCompleteCallback* aEncodeCallback);
+
+  // Extract an Image asynchronously. Its function is same as ExtractDataAsync
+  // except for the parameters. aImage is the uncompressed data. aEncodeCallback
+  // will be called on main thread when encoding process is success.
+  static nsresult ExtractDataFromLayersImageAsync(nsAString& aType,
+                                                  const nsAString& aOptions,
+                                                  bool aUsingCustomOptions,
+                                                  layers::Image* aImage,
+                                                  EncodeCompleteCallback* aEncodeCallback);
 
   // Gives you a stream containing the image represented by aImageBuffer.
   // The format is given in aFormat, for example
   // imgIEncoder::INPUT_FORMAT_HOSTARGB.
   static nsresult GetInputStream(int32_t aWidth,
                                  int32_t aHeight,
                                  uint8_t* aImageBuffer,
                                  int32_t aFormat,
@@ -68,26 +82,42 @@ public:
 private:
   // When called asynchronously, aContext is null.
   static nsresult
   ExtractDataInternal(const nsAString& aType,
                       const nsAString& aOptions,
                       uint8_t* aImageBuffer,
                       int32_t aFormat,
                       const nsIntSize aSize,
+                      layers::Image* aImage,
                       nsICanvasRenderingContextInternal* aContext,
                       nsIInputStream** aStream,
                       imgIEncoder* aEncoder);
 
   // Creates and returns an encoder instance of the type specified in aType.
   // aType may change to "image/png" if no instance of the original type could
   // be created and we had to fall back to a PNG encoder. A null return value
   // should be interpreted as NS_IMAGELIB_ERROR_NO_ENCODER and aType is
   // undefined in this case.
   static already_AddRefed<imgIEncoder> GetImageEncoder(nsAString& aType);
 
   friend class EncodingRunnable;
 };
 
+/**
+ *  The callback interface of ExtractDataAsync and ExtractDataFromLayersImageAsync.
+ *  ReceiveBlob() is called on main thread when encoding is complete.
+ */
+class EncodeCompleteCallback
+{
+public:
+  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(EncodeCompleteCallback)
+
+  virtual nsresult ReceiveBlob(already_AddRefed<DOMFile> aBlob) = 0;
+
+protected:
+  virtual ~EncodeCompleteCallback() {}
+};
+
 } // namespace dom
 } // namespace mozilla
 
 #endif // ImageEncoder_h
--- a/dom/base/moz.build
+++ b/dom/base/moz.build
@@ -48,16 +48,17 @@ EXPORTS += [
 
 EXPORTS.mozilla.dom += [
     'BarProps.h',
     'Console.h',
     'DOMCursor.h',
     'DOMError.h',
     'DOMException.h',
     'DOMRequest.h',
+    'ImageEncoder.h',
     'MessageChannel.h',
     'MessagePort.h',
     'MessagePortList.h',
     'NameSpaceConstants.h',
     'Navigator.h',
     'NodeInfo.h',
     'NodeInfoInlines.h',
     'PerformanceEntry.h',
@@ -74,16 +75,17 @@ UNIFIED_SOURCES += [
     'BarProps.cpp',
     'CompositionStringSynthesizer.cpp',
     'Console.cpp',
     'Crypto.cpp',
     'DOMCursor.cpp',
     'DOMError.cpp',
     'DOMException.cpp',
     'DOMRequest.cpp',
+    'ImageEncoder.cpp',
     'MessageChannel.cpp',
     'MessagePortList.cpp',
     'Navigator.cpp',
     'NodeInfo.cpp',
     'nsContentPermissionHelper.cpp',
     'nsDOMClassInfo.cpp',
     'nsDOMNavigationTiming.cpp',
     'nsDOMScriptObjectFactory.cpp',
--- a/dom/canvas/moz.build
+++ b/dom/canvas/moz.build
@@ -28,17 +28,16 @@ EXPORTS.mozilla.dom += [
 # Canvas 2D and common sources
 UNIFIED_SOURCES += [
     'CanvasImageCache.cpp',
     'CanvasRenderingContext2D.cpp',
     'CanvasUtils.cpp',
     'DocumentRendererChild.cpp',
     'DocumentRendererParent.cpp',
     'ImageData.cpp',
-    'ImageEncoder.cpp',
 ]
 
 # WebGL Sources
 UNIFIED_SOURCES += [
     'MurmurHash3.cpp',
     'WebGL1Context.cpp',
     'WebGL2Context.cpp',
     'WebGLActiveInfo.cpp',