/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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 "DrawTargetWebglInternal.h" #include "SourceSurfaceWebgl.h" #include "mozilla/ClearOnShutdown.h" #include "mozilla/StaticPrefs_gfx.h" #include "mozilla/gfx/AAStroke.h" #include "mozilla/gfx/Blur.h" #include "mozilla/gfx/DrawTargetSkia.h" #include "mozilla/gfx/gfxVars.h" #include "mozilla/gfx/Helpers.h" #include "mozilla/gfx/HelpersSkia.h" #include "mozilla/gfx/Logging.h" #include "mozilla/gfx/PathSkia.h" #include "mozilla/gfx/Swizzle.h" #include "mozilla/layers/ImageDataSerializer.h" #include "skia/include/core/SkPixmap.h" #include "ClientWebGLContext.h" #include "WebGLChild.h" #include "gfxPlatform.h" namespace mozilla::gfx { // Inserts (allocates) a rectangle of the requested size into the tree. Maybe TexturePacker::Insert(const IntSize& aSize) { // Check if the available space could possibly fit the requested size. If // not, there is no reason to continue searching within this sub-tree. if (mAvailable < std::min(aSize.width, aSize.height) || mBounds.width < aSize.width || mBounds.height < aSize.height) { return Nothing(); } if (mChildren) { // If this node has children, then try to insert into each of the children // in turn. Maybe inserted = mChildren[0].Insert(aSize); if (!inserted) { inserted = mChildren[1].Insert(aSize); } // If the insertion succeeded, adjust the available state to reflect the // remaining space in the children. if (inserted) { mAvailable = std::max(mChildren[0].mAvailable, mChildren[1].mAvailable); if (!mAvailable) { DiscardChildren(); } } return inserted; } // If we get here, we've encountered a leaf node. First check if its size is // exactly the requested size. If so, mark the node as unavailable and return // its offset. if (mBounds.Size() == aSize) { mAvailable = 0; return Some(mBounds.TopLeft()); } // The node is larger than the requested size. Choose the axis which has the // most excess space beyond the requested size and split it so that at least // one of the children matches the requested size for that axis. if (mBounds.width - aSize.width > mBounds.height - aSize.height) { mChildren.reset(new TexturePacker[2]{ TexturePacker( IntRect(mBounds.x, mBounds.y, aSize.width, mBounds.height)), TexturePacker(IntRect(mBounds.x + aSize.width, mBounds.y, mBounds.width - aSize.width, mBounds.height))}); } else { mChildren.reset(new TexturePacker[2]{ TexturePacker( IntRect(mBounds.x, mBounds.y, mBounds.width, aSize.height)), TexturePacker(IntRect(mBounds.x, mBounds.y + aSize.height, mBounds.width, mBounds.height - aSize.height))}); } // After splitting, try to insert into the first child, which should usually // be big enough to accomodate the request. Adjust the available state to the // remaining space. Maybe inserted = mChildren[0].Insert(aSize); mAvailable = std::max(mChildren[0].mAvailable, mChildren[1].mAvailable); return inserted; } // Removes (frees) a rectangle with the given bounds from the tree. bool TexturePacker::Remove(const IntRect& aBounds) { if (!mChildren) { // If there are no children, we encountered a leaf node. Non-zero available // state means that this node was already removed previously. Also, if the // bounds don't contain the request, and assuming the tree was previously // split during insertion, then this node is not the node we're searching // for. if (mAvailable > 0 || !mBounds.Contains(aBounds)) { return false; } // The bounds match exactly and it was previously inserted, so in this case // we can just remove it. if (mBounds == aBounds) { mAvailable = std::min(mBounds.width, mBounds.height); return true; } // We need to split this leaf node so that it can exactly match the removed // bounds. We know the leaf node at least contains the removed bounds, but // needs to be subdivided until it has a child node that exactly matches. // Choose the axis to split with the largest amount of excess space. Within // that axis, choose the larger of the space before or after the subrect as // the split point to the new children. if (mBounds.width - aBounds.width > mBounds.height - aBounds.height) { int split = aBounds.x - mBounds.x > mBounds.XMost() - aBounds.XMost() ? aBounds.x : aBounds.XMost(); mChildren.reset(new TexturePacker[2]{ TexturePacker( IntRect(mBounds.x, mBounds.y, split - mBounds.x, mBounds.height), false), TexturePacker(IntRect(split, mBounds.y, mBounds.XMost() - split, mBounds.height), false)}); } else { int split = aBounds.y - mBounds.y > mBounds.YMost() - aBounds.YMost() ? aBounds.y : aBounds.YMost(); mChildren.reset(new TexturePacker[2]{ TexturePacker( IntRect(mBounds.x, mBounds.y, mBounds.width, split - mBounds.y), false), TexturePacker( IntRect(mBounds.x, split, mBounds.width, mBounds.YMost() - split), false)}); } } // We've encountered a branch node. Determine which of the two child nodes // would possibly contain the removed bounds. We first check which axis the // children were split on and then whether the removed bounds on that axis // are past the start of the second child. Proceed to recurse into that // child node for removal. bool next = mChildren[0].mBounds.x < mChildren[1].mBounds.x ? aBounds.x >= mChildren[1].mBounds.x : aBounds.y >= mChildren[1].mBounds.y; bool removed = mChildren[next ? 1 : 0].Remove(aBounds); if (removed) { if (mChildren[0].IsFullyAvailable() && mChildren[1].IsFullyAvailable()) { DiscardChildren(); mAvailable = std::min(mBounds.width, mBounds.height); } else { mAvailable = std::max(mChildren[0].mAvailable, mChildren[1].mAvailable); } } return removed; } SharedTexture::SharedTexture(const IntSize& aSize, SurfaceFormat aFormat, const RefPtr& aTexture) : mPacker(IntRect(IntPoint(0, 0), aSize)), mFormat(aFormat), mTexture(aTexture) {} SharedTextureHandle::SharedTextureHandle(const IntRect& aBounds, SharedTexture* aTexture) : mBounds(aBounds), mTexture(aTexture) {} already_AddRefed SharedTexture::Allocate( const IntSize& aSize) { RefPtr handle; if (Maybe origin = mPacker.Insert(aSize)) { handle = new SharedTextureHandle(IntRect(*origin, aSize), this); ++mAllocatedHandles; } return handle.forget(); } bool SharedTexture::Free(const SharedTextureHandle& aHandle) { if (aHandle.mTexture != this) { return false; } if (!mPacker.Remove(aHandle.mBounds)) { return false; } --mAllocatedHandles; return true; } StandaloneTexture::StandaloneTexture(const IntSize& aSize, SurfaceFormat aFormat, const RefPtr& aTexture) : mSize(aSize), mFormat(aFormat), mTexture(aTexture) {} DrawTargetWebgl::DrawTargetWebgl() = default; inline void DrawTargetWebgl::SharedContext::ClearLastTexture() { mLastTexture = nullptr; mLastClipMask = nullptr; } // Attempts to clear the snapshot state. If the snapshot is only referenced by // this target, then it should simply be destroyed. If it is a WebGL surface in // use by something else, then special cleanup such as reusing the texture or // copy-on-write may be possible. void DrawTargetWebgl::ClearSnapshot(bool aCopyOnWrite, bool aNeedHandle) { if (!mSnapshot) { return; } mSharedContext->ClearLastTexture(); if (mSnapshot->hasOneRef() || mSnapshot->GetType() != SurfaceType::WEBGL) { mSnapshot = nullptr; return; } RefPtr snapshot = mSnapshot.forget().downcast(); if (aCopyOnWrite) { // WebGL snapshots must be notified that the framebuffer contents will be // changing so that it can copy the data. snapshot->DrawTargetWillChange(aNeedHandle); } else { // If not copying, then give the backing texture to the surface for reuse. snapshot->GiveTexture( mSharedContext->WrapSnapshot(GetSize(), GetFormat(), mTex.forget())); } } DrawTargetWebgl::~DrawTargetWebgl() { ClearSnapshot(false); if (mSharedContext) { if (mShmem.IsWritable()) { // Force any Skia snapshots to copy the shmem before it deallocs. mSkia->DetachAllSnapshots(); // Ensure we're done using the shmem before dealloc. mSharedContext->WaitForShmem(this); auto* child = mSharedContext->mWebgl->GetChild(); if (child && child->CanSend()) { child->DeallocShmem(mShmem); } } if (mClipMask) { mSharedContext->mWebgl->DeleteTexture(mClipMask); } if (mFramebuffer) { mSharedContext->mWebgl->DeleteFramebuffer(mFramebuffer); } if (mTex) { mSharedContext->mWebgl->DeleteTexture(mTex); } mSharedContext->mWebgl->Flush(false); } } DrawTargetWebgl::SharedContext::SharedContext() = default; DrawTargetWebgl::SharedContext::~SharedContext() { if (sSharedContext.init() && sSharedContext.get() == this) { sSharedContext.set(nullptr); } // Detect context loss before deletion. if (mWebgl) { mWebgl->ActiveTexture(LOCAL_GL_TEXTURE0); } ClearAllTextures(); UnlinkSurfaceTextures(); UnlinkGlyphCaches(); } // Remove any SourceSurface user data associated with this TextureHandle. inline void DrawTargetWebgl::SharedContext::UnlinkSurfaceTexture( const RefPtr& aHandle) { if (SourceSurface* surface = aHandle->GetSurface()) { // Ensure any WebGL snapshot textures get unlinked. if (surface->GetType() == SurfaceType::WEBGL) { static_cast(surface)->OnUnlinkTexture(this); } surface->RemoveUserData(aHandle->IsShadow() ? &mShadowTextureKey : &mTextureHandleKey); } } // Unlinks TextureHandles from any SourceSurface user data. void DrawTargetWebgl::SharedContext::UnlinkSurfaceTextures() { for (RefPtr handle = mTextureHandles.getFirst(); handle; handle = handle->getNext()) { UnlinkSurfaceTexture(handle); } } // Unlinks GlyphCaches from any ScaledFont user data. void DrawTargetWebgl::SharedContext::UnlinkGlyphCaches() { GlyphCache* cache = mGlyphCaches.getFirst(); while (cache) { ScaledFont* font = cache->GetFont(); // Access the next cache before removing the user data, as it might destroy // the cache. cache = cache->getNext(); font->RemoveUserData(&mGlyphCacheKey); } } void DrawTargetWebgl::SharedContext::OnMemoryPressure() { mShouldClearCaches = true; } // Clear out the entire list of texture handles from any source. void DrawTargetWebgl::SharedContext::ClearAllTextures() { while (!mTextureHandles.isEmpty()) { PruneTextureHandle(mTextureHandles.popLast()); --mNumTextureHandles; } } // Scan through the shared texture pages looking for any that are empty and // delete them. void DrawTargetWebgl::SharedContext::ClearEmptyTextureMemory() { for (auto pos = mSharedTextures.begin(); pos != mSharedTextures.end();) { if (!(*pos)->HasAllocatedHandles()) { RefPtr shared = *pos; size_t usedBytes = shared->UsedBytes(); mEmptyTextureMemory -= usedBytes; mTotalTextureMemory -= usedBytes; pos = mSharedTextures.erase(pos); mWebgl->DeleteTexture(shared->GetWebGLTexture()); } else { ++pos; } } } // If there is a request to clear out the caches because of memory pressure, // then first clear out all the texture handles in the texture cache. If there // are still empty texture pages being kept around, then clear those too. void DrawTargetWebgl::SharedContext::ClearCachesIfNecessary() { if (!mShouldClearCaches.exchange(false)) { return; } mZeroBuffer = nullptr; ClearAllTextures(); if (mEmptyTextureMemory) { ClearEmptyTextureMemory(); } ClearLastTexture(); } // If a non-recoverable error occurred that would stop the canvas from initing. static Atomic sContextInitError(false); MOZ_THREAD_LOCAL(DrawTargetWebgl::SharedContext*) DrawTargetWebgl::sSharedContext; RefPtr DrawTargetWebgl::sMainSharedContext; // Try to initialize a new WebGL context. Verifies that the requested size does // not exceed the available texture limits and that shader creation succeeded. bool DrawTargetWebgl::Init(const IntSize& size, const SurfaceFormat format) { MOZ_ASSERT(format == SurfaceFormat::B8G8R8A8 || format == SurfaceFormat::B8G8R8X8); mSize = size; mFormat = format; if (!sSharedContext.init()) { return false; } DrawTargetWebgl::SharedContext* sharedContext = sSharedContext.get(); if (!sharedContext || sharedContext->IsContextLost()) { mSharedContext = new DrawTargetWebgl::SharedContext; if (!mSharedContext->Initialize()) { mSharedContext = nullptr; return false; } sSharedContext.set(mSharedContext.get()); if (NS_IsMainThread()) { // Keep the shared context alive for the main thread by adding a ref. // Ensure the ref will get cleared on shutdown so it doesn't leak. if (!sMainSharedContext) { ClearOnShutdown(&sMainSharedContext); } sMainSharedContext = mSharedContext; } } else { mSharedContext = sharedContext; } if (size_t(std::max(size.width, size.height)) > mSharedContext->mMaxTextureSize) { return false; } if (!CreateFramebuffer()) { return false; } auto* child = mSharedContext->mWebgl->GetChild(); if (child && child->CanSend()) { size_t byteSize = layers::ImageDataSerializer::ComputeRGBBufferSize( mSize, SurfaceFormat::B8G8R8A8); if (byteSize) { (void)child->AllocUnsafeShmem(byteSize, &mShmem); } } mSkia = new DrawTargetSkia; if (mShmem.IsWritable()) { auto stride = layers::ImageDataSerializer::ComputeRGBStride( SurfaceFormat::B8G8R8A8, size.width); if (!mSkia->Init(mShmem.get(), size, stride, SurfaceFormat::B8G8R8A8, true)) { return false; } } else if (!mSkia->Init(size, SurfaceFormat::B8G8R8A8)) { return false; } // Allocate an unclipped copy of the DT pointing to its data. uint8_t* dtData = nullptr; IntSize dtSize; int32_t dtStride = 0; SurfaceFormat dtFormat = SurfaceFormat::UNKNOWN; if (!mSkia->LockBits(&dtData, &dtSize, &dtStride, &dtFormat)) { return false; } mSkiaNoClip = new DrawTargetSkia; if (!mSkiaNoClip->Init(dtData, dtSize, dtStride, dtFormat, true)) { mSkia->ReleaseBits(dtData); return false; } mSkia->ReleaseBits(dtData); SetPermitSubpixelAA(IsOpaque(format)); return true; } bool DrawTargetWebgl::SharedContext::Initialize() { WebGLContextOptions options = {}; options.alpha = true; options.depth = false; options.stencil = false; options.antialias = false; options.preserveDrawingBuffer = true; options.failIfMajorPerformanceCaveat = true; mWebgl = new ClientWebGLContext(true); mWebgl->SetContextOptions(options); if (mWebgl->SetDimensions(1, 1) != NS_OK) { // There was a non-recoverable error when trying to create a host context. sContextInitError = true; mWebgl = nullptr; return false; } if (mWebgl->IsContextLost()) { mWebgl = nullptr; return false; } mMaxTextureSize = mWebgl->Limits().maxTex2dSize; if (kIsMacOS) { mRasterizationTruncates = mWebgl->Vendor() == gl::GLVendor::ATI; } CachePrefs(); if (!CreateShaders()) { // There was a non-recoverable error when trying to init shaders. sContextInitError = true; mWebgl = nullptr; return false; } return true; } void DrawTargetWebgl::SharedContext::SetBlendState( CompositionOp aOp, const Maybe& aColor) { if (aOp == mLastCompositionOp && mLastBlendColor == aColor) { return; } mLastCompositionOp = aOp; mLastBlendColor = aColor; // AA is not supported for all composition ops, so switching blend modes may // cause a toggle in AA state. Certain ops such as OP_SOURCE require output // alpha that is blended separately from AA coverage. This would require two // stage blending which can incur a substantial performance penalty, so to // work around this currently we just disable AA for those ops. mDirtyAA = true; // Map the composition op to a WebGL blend mode, if possible. mWebgl->Enable(LOCAL_GL_BLEND); switch (aOp) { case CompositionOp::OP_OVER: if (aColor) { // If a color is supplied, then we blend subpixel text. mWebgl->BlendColor(aColor->b, aColor->g, aColor->r, 1.0f); mWebgl->BlendFunc(LOCAL_GL_CONSTANT_COLOR, LOCAL_GL_ONE_MINUS_SRC_COLOR); } else { mWebgl->BlendFunc(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA); } break; case CompositionOp::OP_ADD: mWebgl->BlendFunc(LOCAL_GL_ONE, LOCAL_GL_ONE); break; case CompositionOp::OP_ATOP: mWebgl->BlendFunc(LOCAL_GL_DST_ALPHA, LOCAL_GL_ONE_MINUS_SRC_ALPHA); break; case CompositionOp::OP_SOURCE: if (aColor) { // If a color is supplied, then we assume there is clipping or AA. This // requires that we still use an over blend func with the clip/AA alpha, // while filling the interior with the unaltered color. Normally this // would require dual source blending, but we can emulate it with only // a blend color. mWebgl->BlendColor(aColor->b, aColor->g, aColor->r, aColor->a); mWebgl->BlendFunc(LOCAL_GL_CONSTANT_COLOR, LOCAL_GL_ONE_MINUS_SRC_COLOR); } else { mWebgl->Disable(LOCAL_GL_BLEND); } break; case CompositionOp::OP_CLEAR: // Assume the source is an alpha mask for clearing. Be careful to blend in // the correct alpha if the target is opaque. mWebgl->BlendFuncSeparate( LOCAL_GL_ZERO, LOCAL_GL_ONE_MINUS_SRC_ALPHA, IsOpaque(mCurrentTarget->GetFormat()) ? LOCAL_GL_ONE : LOCAL_GL_ZERO, LOCAL_GL_ONE_MINUS_SRC_ALPHA); break; default: mWebgl->Disable(LOCAL_GL_BLEND); break; } } // Ensure the WebGL framebuffer is set to the current target. bool DrawTargetWebgl::SharedContext::SetTarget(DrawTargetWebgl* aDT) { if (!mWebgl || mWebgl->IsContextLost()) { return false; } if (aDT != mCurrentTarget) { mCurrentTarget = aDT; if (aDT) { mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, aDT->mFramebuffer); mViewportSize = aDT->GetSize(); mWebgl->Viewport(0, 0, mViewportSize.width, mViewportSize.height); // Force the viewport to be reset. mDirtyViewport = true; } } return true; } // Replace the current clip rect with a new potentially-AA'd clip rect. void DrawTargetWebgl::SharedContext::SetClipRect(const Rect& aClipRect) { // Only invalidate the clip rect if it actually changes. if (!mClipAARect.IsEqualEdges(aClipRect)) { mClipAARect = aClipRect; // Store the integer-aligned bounds. mClipRect = RoundedOut(aClipRect); // Notify the shader uniform it needs to update. mDirtyClip = true; } } bool DrawTargetWebgl::SharedContext::SetClipMask( const RefPtr& aTex) { if (mLastClipMask != aTex) { if (!mWebgl) { return false; } mWebgl->ActiveTexture(LOCAL_GL_TEXTURE1); mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, aTex); mWebgl->ActiveTexture(LOCAL_GL_TEXTURE0); mLastClipMask = aTex; } return true; } bool DrawTargetWebgl::SharedContext::SetNoClipMask() { if (mNoClipMask) { return SetClipMask(mNoClipMask); } if (!mWebgl) { return false; } mNoClipMask = mWebgl->CreateTexture(); if (!mNoClipMask) { return false; } mWebgl->ActiveTexture(LOCAL_GL_TEXTURE1); mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, mNoClipMask); static const uint8_t solidMask[4] = {0xFF, 0xFF, 0xFF, 0xFF}; mWebgl->RawTexImage( 0, LOCAL_GL_RGBA8, {0, 0, 0}, {LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE}, {LOCAL_GL_TEXTURE_2D, {1, 1, 1}, gfxAlphaType::NonPremult, Some(RawBuffer(Range(solidMask, sizeof(solidMask))))}); InitTexParameters(mNoClipMask, false); mWebgl->ActiveTexture(LOCAL_GL_TEXTURE0); mLastClipMask = mNoClipMask; return true; } inline bool DrawTargetWebgl::ClipStack::operator==( const DrawTargetWebgl::ClipStack& aOther) const { // Verify the transform and bounds match. if (!mTransform.FuzzyEquals(aOther.mTransform) || !mRect.IsEqualInterior(aOther.mRect)) { return false; } // Verify the paths match. if (!mPath) { return !aOther.mPath; } if (!aOther.mPath || mPath->GetBackendType() != aOther.mPath->GetBackendType()) { return false; } if (mPath->GetBackendType() != BackendType::SKIA) { return mPath == aOther.mPath; } return static_cast(mPath.get())->GetPath() == static_cast(aOther.mPath.get())->GetPath(); } // If the clip region can't be approximated by a simple clip rect, then we need // to generate a clip mask that can represent the clip region per-pixel. We // render to the Skia target temporarily, transparent outside the clip region, // opaque inside, and upload this to a texture that can be used by the shaders. bool DrawTargetWebgl::GenerateComplexClipMask() { if (!mClipChanged || (mClipMask && mCachedClipStack == mClipStack)) { mClipChanged = false; // If the clip mask was already generated, use the cached mask and bounds. mSharedContext->SetClipMask(mClipMask); mSharedContext->SetClipRect(mClipBounds); return true; } if (!mWebglValid) { // If the Skia target is currently being used, then we can't render the mask // in it. return false; } RefPtr webgl = mSharedContext->mWebgl; if (!webgl) { return false; } bool init = false; if (!mClipMask) { mClipMask = webgl->CreateTexture(); if (!mClipMask) { return false; } init = true; } // Try to get the bounds of the clip to limit the size of the mask. if (Maybe clip = mSkia->GetDeviceClipRect(true)) { mClipBounds = *clip; } else { // If we can't get bounds, then just use the entire viewport. mClipBounds = GetRect(); } mClipAARect = Rect(mClipBounds); // If initializing the clip mask, then allocate the entire texture to ensure // all pixels get filled with an empty mask regardless. Otherwise, restrict // uploading to only the clip region. RefPtr dt = new DrawTargetSkia; if (!dt->Init(mClipBounds.Size(), SurfaceFormat::A8)) { return false; } // Set the clip region and fill the entire inside of it // with opaque white. mCachedClipStack.clear(); for (auto& clipStack : mClipStack) { // Record the current state of the clip stack for this mask. mCachedClipStack.push_back(clipStack); dt->SetTransform( Matrix(clipStack.mTransform).PostTranslate(-mClipBounds.TopLeft())); if (clipStack.mPath) { dt->PushClip(clipStack.mPath); } else { dt->PushClipRect(clipStack.mRect); } } dt->SetTransform(Matrix::Translation(-mClipBounds.TopLeft())); dt->FillRect(Rect(mClipBounds), ColorPattern(DeviceColor(1, 1, 1, 1))); // Bind the clip mask for uploading. webgl->ActiveTexture(LOCAL_GL_TEXTURE1); webgl->BindTexture(LOCAL_GL_TEXTURE_2D, mClipMask); if (init) { mSharedContext->InitTexParameters(mClipMask, false); } RefPtr data; if (RefPtr snapshot = dt->Snapshot()) { data = snapshot->GetDataSurface(); } // Finally, upload the texture data and initialize texture storage if // necessary. if (init && mClipBounds.Size() != mSize) { mSharedContext->UploadSurface(nullptr, SurfaceFormat::A8, GetRect(), IntPoint(), true, true); init = false; } mSharedContext->UploadSurface(data, SurfaceFormat::A8, IntRect(IntPoint(), mClipBounds.Size()), mClipBounds.TopLeft(), init); webgl->ActiveTexture(LOCAL_GL_TEXTURE0); // We already bound the texture, so notify the shared context that the clip // mask changed to it. mSharedContext->mLastClipMask = mClipMask; mSharedContext->SetClipRect(mClipBounds); // We uploaded a surface, just as if we missed the texture cache, so account // for that here. mProfile.OnCacheMiss(); return !!data; } bool DrawTargetWebgl::SetSimpleClipRect() { // Determine whether the clipping rectangle is simple enough to accelerate. // Check if there is a device space clip rectangle available from the Skia // target. if (Maybe clip = mSkia->GetDeviceClipRect(false)) { // If the clip is empty, leave the final integer clip rectangle empty to // trivially discard the draw request. // If the clip rect is larger than the viewport, just set it to the // viewport. if (!clip->IsEmpty() && clip->Contains(GetRect())) { clip = Some(GetRect()); } mSharedContext->SetClipRect(*clip); mSharedContext->SetNoClipMask(); return true; } // There was no pixel-aligned clip rect available, so check the clip stack to // see if there is an AA'd axis-aligned rectangle clip. Rect rect(GetRect()); for (auto& clipStack : mClipStack) { // If clip is a path or it has a non-axis-aligned transform, then it is // complex. if (clipStack.mPath || !clipStack.mTransform.PreservesAxisAlignedRectangles()) { return false; } // Transform the rect and intersect it with the current clip. rect = clipStack.mTransform.TransformBounds(clipStack.mRect).Intersect(rect); } mSharedContext->SetClipRect(rect); mSharedContext->SetNoClipMask(); return true; } // Installs the Skia clip rectangle, if applicable, onto the shared WebGL // context as well as sets the WebGL framebuffer to the current target. bool DrawTargetWebgl::PrepareContext(bool aClipped) { if (!aClipped) { // If no clipping requested, just set the clip rect to the viewport. mSharedContext->SetClipRect(GetRect()); mSharedContext->SetNoClipMask(); // Ensure the clip gets reset if clipping is later requested for the target. mRefreshClipState = true; } else if (mRefreshClipState || !mSharedContext->IsCurrentTarget(this)) { // Try to use a simple clip rect if possible. Otherwise, fall back to // generating a clip mask texture that can represent complex clip regions. if (!SetSimpleClipRect() && !GenerateComplexClipMask()) { return false; } mClipChanged = false; mRefreshClipState = false; } return mSharedContext->SetTarget(this); } bool DrawTargetWebgl::SharedContext::IsContextLost() const { return !mWebgl || mWebgl->IsContextLost(); } // Signal to CanvasRenderingContext2D when the WebGL context is lost. bool DrawTargetWebgl::IsValid() const { return mSharedContext && !mSharedContext->IsContextLost(); } already_AddRefed DrawTargetWebgl::Create( const IntSize& aSize, SurfaceFormat aFormat) { if (!gfxVars::UseAcceleratedCanvas2D()) { return nullptr; } // If context initialization would fail, don't even try to create a context. if (sContextInitError) { return nullptr; } if (!Factory::AllowedSurfaceSize(aSize)) { return nullptr; } // The interpretation of the min-size and max-size follows from the old // SkiaGL prefs. First just ensure that the context is not unreasonably // small. static const int32_t kMinDimension = 16; if (std::min(aSize.width, aSize.height) < kMinDimension) { return nullptr; } int32_t minSize = StaticPrefs::gfx_canvas_accelerated_min_size(); if (aSize.width * aSize.height < minSize * minSize) { return nullptr; } // Maximum pref allows 3 different options: // 0 means unlimited size, // > 0 means use value as an absolute threshold, // < 0 means use the number of screen pixels as a threshold. int32_t maxSize = StaticPrefs::gfx_canvas_accelerated_max_size(); if (maxSize > 0) { if (std::max(aSize.width, aSize.height) > maxSize) { return nullptr; } } else if (maxSize < 0) { // Default to historical mobile screen size of 980x480, like FishIEtank. // In addition, allow acceleration up to this size even if the screen is // smaller. A lot content expects this size to work well. See Bug 999841 static const int32_t kScreenPixels = 980 * 480; IntSize screenSize = gfxPlatform::GetPlatform()->GetScreenSize(); if (aSize.width * aSize.height > std::max(screenSize.width * screenSize.height, kScreenPixels)) { return nullptr; } } RefPtr dt = new DrawTargetWebgl; if (!dt->Init(aSize, aFormat) || !dt->IsValid()) { return nullptr; } return dt.forget(); } void* DrawTargetWebgl::GetNativeSurface(NativeSurfaceType aType) { switch (aType) { case NativeSurfaceType::WEBGL_CONTEXT: // If the context is lost, then don't attempt to access it. if (mSharedContext->IsContextLost()) { return nullptr; } if (!mWebglValid) { FlushFromSkia(); } return mSharedContext->mWebgl.get(); default: return nullptr; } } // Wrap a WebGL texture holding a snapshot with a texture handle. Note that // while the texture is still in use as the backing texture of a framebuffer, // it's texture memory is not currently tracked with other texture handles. // Once it is finally orphaned and used as a texture handle, it must be added // to the resource usage totals. already_AddRefed DrawTargetWebgl::SharedContext::WrapSnapshot( const IntSize& aSize, SurfaceFormat aFormat, RefPtr aTex) { // Ensure there is enough space for the texture. size_t usedBytes = TextureHandle::UsedBytes(aFormat, aSize); PruneTextureMemory(usedBytes, false); // Allocate a handle for the texture RefPtr handle = new StandaloneTexture(aSize, aFormat, aTex.forget()); mStandaloneTextures.push_back(handle); mTextureHandles.insertFront(handle); mTotalTextureMemory += usedBytes; mUsedTextureMemory += usedBytes; ++mNumTextureHandles; return handle.forget(); } void DrawTargetWebgl::SharedContext::SetTexFilter(WebGLTextureJS* aTex, bool aFilter) { mWebgl->TexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER, aFilter ? LOCAL_GL_LINEAR : LOCAL_GL_NEAREST); mWebgl->TexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER, aFilter ? LOCAL_GL_LINEAR : LOCAL_GL_NEAREST); } void DrawTargetWebgl::SharedContext::InitTexParameters(WebGLTextureJS* aTex, bool aFilter) { mWebgl->TexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S, LOCAL_GL_CLAMP_TO_EDGE); mWebgl->TexParameteri(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T, LOCAL_GL_CLAMP_TO_EDGE); SetTexFilter(aTex, aFilter); } // Copy the contents of the WebGL framebuffer into a WebGL texture. already_AddRefed DrawTargetWebgl::SharedContext::CopySnapshot( const IntRect& aRect, TextureHandle* aHandle) { if (!mWebgl || mWebgl->IsContextLost()) { return nullptr; } // If the target is going away, then we can just directly reuse the // framebuffer texture since it will never change. RefPtr tex = mWebgl->CreateTexture(); if (!tex) { return nullptr; } // If copying from a non-DT source, we have to bind a scratch framebuffer for // reading. if (aHandle) { if (!mScratchFramebuffer) { mScratchFramebuffer = mWebgl->CreateFramebuffer(); } mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mScratchFramebuffer); mWebgl->FramebufferTexture2D( LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0, LOCAL_GL_TEXTURE_2D, aHandle->GetWebGLTexture(), 0); } // Create a texture to hold the copy mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, tex); mWebgl->TexStorage2D(LOCAL_GL_TEXTURE_2D, 1, LOCAL_GL_RGBA8, aRect.width, aRect.height); InitTexParameters(tex); // Copy the framebuffer into the texture mWebgl->CopyTexSubImage2D(LOCAL_GL_TEXTURE_2D, 0, 0, 0, aRect.x, aRect.y, aRect.width, aRect.height); ClearLastTexture(); SurfaceFormat format = aHandle ? aHandle->GetFormat() : mCurrentTarget->GetFormat(); already_AddRefed result = WrapSnapshot(aRect.Size(), format, tex.forget()); // Restore the actual framebuffer after reading is done. if (aHandle && mCurrentTarget) { mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mCurrentTarget->mFramebuffer); } return result; } inline DrawTargetWebgl::AutoRestoreContext::AutoRestoreContext( DrawTargetWebgl* aTarget) : mTarget(aTarget), mClipAARect(aTarget->mSharedContext->mClipAARect), mLastClipMask(aTarget->mSharedContext->mLastClipMask) {} inline DrawTargetWebgl::AutoRestoreContext::~AutoRestoreContext() { mTarget->mSharedContext->SetClipRect(mClipAARect); if (mLastClipMask) { mTarget->mSharedContext->SetClipMask(mLastClipMask); } mTarget->mRefreshClipState = true; } // Utility method to install the target before copying a snapshot. already_AddRefed DrawTargetWebgl::CopySnapshot( const IntRect& aRect) { AutoRestoreContext restore(this); if (!PrepareContext(false)) { return nullptr; } return mSharedContext->CopySnapshot(aRect); } // Borrow a snapshot that may be used by another thread for composition. Only // Skia snapshots are safe to pass around. already_AddRefed DrawTargetWebgl::GetDataSnapshot() { if (!mSkiaValid) { ReadIntoSkia(); } else if (mSkiaLayer) { FlattenSkia(); } return mSkia->Snapshot(mFormat); } already_AddRefed DrawTargetWebgl::Snapshot() { // If already using the Skia fallback, then just snapshot that. if (mSkiaValid) { return GetDataSnapshot(); } // There's no valid Skia snapshot, so we need to get one from the WebGL // context. if (!mSnapshot) { // Create a copy-on-write reference to this target. mSnapshot = new SourceSurfaceWebgl(this); } return do_AddRef(mSnapshot); } // If we need to provide a snapshot for another DrawTargetWebgl that shares the // same WebGL context, then it is safe to directly return a snapshot. Otherwise, // we may be exporting to another thread and require a data snapshot. already_AddRefed DrawTargetWebgl::GetOptimizedSnapshot( DrawTarget* aTarget) { if (aTarget && aTarget->GetBackendType() == BackendType::WEBGL && static_cast(aTarget)->mSharedContext == mSharedContext) { return Snapshot(); } return GetDataSnapshot(); } // Read from the WebGL context into a buffer. This handles both swizzling BGRA // to RGBA and flipping the image. bool DrawTargetWebgl::SharedContext::ReadInto(uint8_t* aDstData, int32_t aDstStride, SurfaceFormat aFormat, const IntRect& aBounds, TextureHandle* aHandle) { MOZ_ASSERT(aFormat == SurfaceFormat::B8G8R8A8 || aFormat == SurfaceFormat::B8G8R8X8); // If reading into a new texture, we have to bind it to a scratch framebuffer // for reading. if (aHandle) { if (!mScratchFramebuffer) { mScratchFramebuffer = mWebgl->CreateFramebuffer(); } mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mScratchFramebuffer); mWebgl->FramebufferTexture2D( LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0, LOCAL_GL_TEXTURE_2D, aHandle->GetWebGLTexture(), 0); } else if (mCurrentTarget && mCurrentTarget->mIsClear) { // If reading from a target that is still clear, then avoid the readback by // just clearing the data. SkPixmap(MakeSkiaImageInfo(aBounds.Size(), aFormat), aDstData, aDstStride) .erase(IsOpaque(aFormat) ? SK_ColorBLACK : SK_ColorTRANSPARENT); return true; } webgl::ReadPixelsDesc desc; desc.srcOffset = *ivec2::From(aBounds); desc.size = *uvec2::FromSize(aBounds); desc.packState.rowLength = aDstStride / 4; bool success = false; if (mCurrentTarget && mCurrentTarget->mShmem.IsWritable() && aDstData == mCurrentTarget->mShmem.get()) { success = mWebgl->DoReadPixels(desc, mCurrentTarget->mShmem); } else { Range range = {aDstData, size_t(aDstStride) * aBounds.height}; success = mWebgl->DoReadPixels(desc, range); } // Restore the actual framebuffer after reading is done. if (aHandle && mCurrentTarget) { mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mCurrentTarget->mFramebuffer); } return success; } already_AddRefed DrawTargetWebgl::SharedContext::ReadSnapshot(TextureHandle* aHandle) { // Allocate a data surface, map it, and read from the WebGL context into the // surface. SurfaceFormat format = SurfaceFormat::UNKNOWN; IntRect bounds; if (aHandle) { format = aHandle->GetFormat(); bounds = aHandle->GetBounds(); } else { format = mCurrentTarget->GetFormat(); bounds = mCurrentTarget->GetRect(); } RefPtr surface = Factory::CreateDataSourceSurface(bounds.Size(), format); if (!surface) { return nullptr; } DataSourceSurface::ScopedMap dstMap(surface, DataSourceSurface::WRITE); if (!dstMap.IsMapped() || !ReadInto(dstMap.GetData(), dstMap.GetStride(), format, bounds, aHandle)) { return nullptr; } return surface.forget(); } // Utility method to install the target before reading a snapshot. bool DrawTargetWebgl::ReadInto(uint8_t* aDstData, int32_t aDstStride) { if (!PrepareContext(false)) { return false; } return mSharedContext->ReadInto(aDstData, aDstStride, GetFormat(), GetRect()); } // Utility method to install the target before reading a snapshot. already_AddRefed DrawTargetWebgl::ReadSnapshot() { AutoRestoreContext restore(this); if (!PrepareContext(false)) { return nullptr; } mProfile.OnReadback(); return mSharedContext->ReadSnapshot(); } already_AddRefed DrawTargetWebgl::GetBackingSurface() { return Snapshot(); } void DrawTargetWebgl::DetachAllSnapshots() { mSkia->DetachAllSnapshots(); ClearSnapshot(); } // Prepare the framebuffer for accelerated drawing. Any cached snapshots will // be invalidated if not detached and copied here. Ensure the WebGL // framebuffer's contents are updated if still somehow stored in the Skia // framebuffer. bool DrawTargetWebgl::MarkChanged() { if (mSnapshot) { // Try to copy the target into a new texture if possible. ClearSnapshot(true, true); } if (!mWebglValid && !FlushFromSkia()) { return false; } mSkiaValid = false; mIsClear = false; return true; } bool DrawTargetWebgl::LockBits(uint8_t** aData, IntSize* aSize, int32_t* aStride, SurfaceFormat* aFormat, IntPoint* aOrigin) { // Can only access pixels if there is valid, flattened Skia data. if (mSkiaValid && !mSkiaLayer) { MarkSkiaChanged(); return mSkia->LockBits(aData, aSize, aStride, aFormat, aOrigin); } return false; } void DrawTargetWebgl::ReleaseBits(uint8_t* aData) { // Can only access pixels if there is valid, flattened Skia data. if (mSkiaValid && !mSkiaLayer) { mSkia->ReleaseBits(aData); } } // Format is x, y, alpha static const float kRectVertexData[12] = {0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f, 1.0f, 1.0f}; // Orphans the contents of the path vertex buffer. The beginning of the buffer // always contains data for a simple rectangle draw to avoid needing to switch // buffers. void DrawTargetWebgl::SharedContext::ResetPathVertexBuffer(bool aChanged) { mWebgl->BindBuffer(LOCAL_GL_ARRAY_BUFFER, mPathVertexBuffer.get()); mWebgl->RawBufferData( LOCAL_GL_ARRAY_BUFFER, nullptr, std::max(size_t(mPathVertexCapacity), sizeof(kRectVertexData)), LOCAL_GL_DYNAMIC_DRAW); mWebgl->RawBufferSubData(LOCAL_GL_ARRAY_BUFFER, 0, (const uint8_t*)kRectVertexData, sizeof(kRectVertexData)); mPathVertexOffset = sizeof(kRectVertexData); if (aChanged) { mWGROutputBuffer.reset( mPathVertexCapacity > 0 ? new (fallible) WGR::OutputVertex[mPathVertexCapacity / sizeof(WGR::OutputVertex)] : nullptr); } } // Attempts to create all shaders and resources to be used for drawing commands. // Returns whether or not this succeeded. bool DrawTargetWebgl::SharedContext::CreateShaders() { if (!mPathVertexArray) { mPathVertexArray = mWebgl->CreateVertexArray(); } if (!mPathVertexBuffer) { mPathVertexBuffer = mWebgl->CreateBuffer(); mWebgl->BindVertexArray(mPathVertexArray.get()); ResetPathVertexBuffer(); mWebgl->EnableVertexAttribArray(0); mWebgl->VertexAttribPointer(0, 3, LOCAL_GL_FLOAT, LOCAL_GL_FALSE, 0, 0); } if (!mSolidProgram) { // AA is computed by using the basis vectors of the transform to determine // both the scale and orientation. The scale is then used to extrude the // rectangle outward by 1 screen-space pixel to account for the AA region. // The distance to the rectangle edges is passed to the fragment shader in // an interpolant, biased by 0.5 so it represents the desired coverage. The // minimum coverage is then chosen by the fragment shader to use as an AA // coverage value to modulate the color. auto vsSource = u"attribute vec3 a_vertex;\n" "uniform vec2 u_transform[3];\n" "uniform vec2 u_viewport;\n" "uniform vec4 u_clipbounds;\n" "uniform float u_aa;\n" "varying vec2 v_cliptc;\n" "varying vec4 v_clipdist;\n" "varying vec4 v_dist;\n" "varying float v_alpha;\n" "void main() {\n" " vec2 scale = vec2(dot(u_transform[0], u_transform[0]),\n" " dot(u_transform[1], u_transform[1]));\n" " vec2 invScale = u_aa * inversesqrt(scale + 1.0e-6);\n" " scale *= invScale;\n" " vec2 extrude = a_vertex.xy + invScale * (2.0 * a_vertex.xy - " "1.0);\n" " vec2 vertex = u_transform[0] * extrude.x +\n" " u_transform[1] * extrude.y +\n" " u_transform[2];\n" " gl_Position = vec4(vertex * 2.0 / u_viewport - 1.0, 0.0, 1.0);\n" " v_cliptc = vertex / u_viewport;\n" " v_clipdist = vec4(vertex - u_clipbounds.xy,\n" " u_clipbounds.zw - vertex);\n" " v_dist = vec4(extrude, 1.0 - extrude) * scale.xyxy + 1.5 - u_aa;\n" " v_alpha = a_vertex.z;\n" "}\n"_ns; auto fsSource = u"precision mediump float;\n" "uniform vec4 u_color;\n" "uniform sampler2D u_clipmask;\n" "varying highp vec2 v_cliptc;\n" "varying vec4 v_clipdist;\n" "varying vec4 v_dist;\n" "varying float v_alpha;\n" "void main() {\n" " float clip = texture2D(u_clipmask, v_cliptc).r;\n" " vec4 dist = min(v_dist, v_clipdist);\n" " dist.xy = min(dist.xy, dist.zw);\n" " float aa = v_alpha * clamp(min(dist.x, dist.y), 0.0, 1.0);\n" " gl_FragColor = clip * aa * u_color;\n" "}\n"_ns; RefPtr vsId = mWebgl->CreateShader(LOCAL_GL_VERTEX_SHADER); mWebgl->ShaderSource(*vsId, vsSource); mWebgl->CompileShader(*vsId); if (!mWebgl->GetCompileResult(*vsId).success) { return false; } RefPtr fsId = mWebgl->CreateShader(LOCAL_GL_FRAGMENT_SHADER); mWebgl->ShaderSource(*fsId, fsSource); mWebgl->CompileShader(*fsId); if (!mWebgl->GetCompileResult(*fsId).success) { return false; } mSolidProgram = mWebgl->CreateProgram(); mWebgl->AttachShader(*mSolidProgram, *vsId); mWebgl->AttachShader(*mSolidProgram, *fsId); mWebgl->BindAttribLocation(*mSolidProgram, 0, u"a_vertex"_ns); mWebgl->LinkProgram(*mSolidProgram); if (!mWebgl->GetLinkResult(*mSolidProgram).success) { return false; } mSolidProgramViewport = mWebgl->GetUniformLocation(*mSolidProgram, u"u_viewport"_ns); mSolidProgramAA = mWebgl->GetUniformLocation(*mSolidProgram, u"u_aa"_ns); mSolidProgramTransform = mWebgl->GetUniformLocation(*mSolidProgram, u"u_transform"_ns); mSolidProgramColor = mWebgl->GetUniformLocation(*mSolidProgram, u"u_color"_ns); mSolidProgramClipMask = mWebgl->GetUniformLocation(*mSolidProgram, u"u_clipmask"_ns); mSolidProgramClipBounds = mWebgl->GetUniformLocation(*mSolidProgram, u"u_clipbounds"_ns); if (!mSolidProgramViewport || !mSolidProgramAA || !mSolidProgramTransform || !mSolidProgramColor || !mSolidProgramClipMask || !mSolidProgramClipBounds) { return false; } mWebgl->UseProgram(mSolidProgram); int32_t clipMaskData = 1; mWebgl->UniformData(LOCAL_GL_INT, mSolidProgramClipMask, false, {(const uint8_t*)&clipMaskData, sizeof(clipMaskData)}); } if (!mImageProgram) { auto vsSource = u"attribute vec3 a_vertex;\n" "uniform vec2 u_viewport;\n" "uniform vec4 u_clipbounds;\n" "uniform float u_aa;\n" "uniform vec2 u_transform[3];\n" "uniform vec2 u_texmatrix[3];\n" "varying vec2 v_cliptc;\n" "varying vec2 v_texcoord;\n" "varying vec4 v_clipdist;\n" "varying vec4 v_dist;\n" "varying float v_alpha;\n" "void main() {\n" " vec2 scale = vec2(dot(u_transform[0], u_transform[0]),\n" " dot(u_transform[1], u_transform[1]));\n" " vec2 invScale = u_aa * inversesqrt(scale + 1.0e-6);\n" " scale *= invScale;\n" " vec2 extrude = a_vertex.xy + invScale * (2.0 * a_vertex.xy - " "1.0);\n" " vec2 vertex = u_transform[0] * extrude.x +\n" " u_transform[1] * extrude.y +\n" " u_transform[2];\n" " gl_Position = vec4(vertex * 2.0 / u_viewport - 1.0, 0.0, 1.0);\n" " v_cliptc = vertex / u_viewport;\n" " v_clipdist = vec4(vertex - u_clipbounds.xy,\n" " u_clipbounds.zw - vertex);\n" " v_texcoord = u_texmatrix[0] * extrude.x +\n" " u_texmatrix[1] * extrude.y +\n" " u_texmatrix[2];\n" " v_dist = vec4(extrude, 1.0 - extrude) * scale.xyxy + 1.5 - u_aa;\n" " v_alpha = a_vertex.z;\n" "}\n"_ns; auto fsSource = u"precision mediump float;\n" "uniform vec4 u_texbounds;\n" "uniform vec4 u_color;\n" "uniform float u_swizzle;\n" "uniform sampler2D u_sampler;\n" "uniform sampler2D u_clipmask;\n" "varying highp vec2 v_cliptc;\n" "varying highp vec2 v_texcoord;\n" "varying vec4 v_clipdist;\n" "varying vec4 v_dist;\n" "varying float v_alpha;\n" "void main() {\n" " highp vec2 tc = clamp(v_texcoord, u_texbounds.xy,\n" " u_texbounds.zw);\n" " vec4 image = texture2D(u_sampler, tc);\n" " float clip = texture2D(u_clipmask, v_cliptc).r;\n" " vec4 dist = min(v_dist, v_clipdist);\n" " dist.xy = min(dist.xy, dist.zw);\n" " float aa = v_alpha * clamp(min(dist.x, dist.y), 0.0, 1.0);\n" " gl_FragColor = clip * aa * u_color *\n" " mix(image, image.rrrr, u_swizzle);\n" "}\n"_ns; RefPtr vsId = mWebgl->CreateShader(LOCAL_GL_VERTEX_SHADER); mWebgl->ShaderSource(*vsId, vsSource); mWebgl->CompileShader(*vsId); if (!mWebgl->GetCompileResult(*vsId).success) { return false; } RefPtr fsId = mWebgl->CreateShader(LOCAL_GL_FRAGMENT_SHADER); mWebgl->ShaderSource(*fsId, fsSource); mWebgl->CompileShader(*fsId); if (!mWebgl->GetCompileResult(*fsId).success) { return false; } mImageProgram = mWebgl->CreateProgram(); mWebgl->AttachShader(*mImageProgram, *vsId); mWebgl->AttachShader(*mImageProgram, *fsId); mWebgl->BindAttribLocation(*mImageProgram, 0, u"a_vertex"_ns); mWebgl->LinkProgram(*mImageProgram); if (!mWebgl->GetLinkResult(*mImageProgram).success) { return false; } mImageProgramViewport = mWebgl->GetUniformLocation(*mImageProgram, u"u_viewport"_ns); mImageProgramAA = mWebgl->GetUniformLocation(*mImageProgram, u"u_aa"_ns); mImageProgramTransform = mWebgl->GetUniformLocation(*mImageProgram, u"u_transform"_ns); mImageProgramTexMatrix = mWebgl->GetUniformLocation(*mImageProgram, u"u_texmatrix"_ns); mImageProgramTexBounds = mWebgl->GetUniformLocation(*mImageProgram, u"u_texbounds"_ns); mImageProgramSwizzle = mWebgl->GetUniformLocation(*mImageProgram, u"u_swizzle"_ns); mImageProgramColor = mWebgl->GetUniformLocation(*mImageProgram, u"u_color"_ns); mImageProgramSampler = mWebgl->GetUniformLocation(*mImageProgram, u"u_sampler"_ns); mImageProgramClipMask = mWebgl->GetUniformLocation(*mImageProgram, u"u_clipmask"_ns); mImageProgramClipBounds = mWebgl->GetUniformLocation(*mImageProgram, u"u_clipbounds"_ns); if (!mImageProgramViewport || !mImageProgramAA || !mImageProgramTransform || !mImageProgramTexMatrix || !mImageProgramTexBounds || !mImageProgramSwizzle || !mImageProgramColor || !mImageProgramSampler || !mImageProgramClipMask || !mImageProgramClipBounds) { return false; } mWebgl->UseProgram(mImageProgram); int32_t samplerData = 0; mWebgl->UniformData(LOCAL_GL_INT, mImageProgramSampler, false, {(const uint8_t*)&samplerData, sizeof(samplerData)}); int32_t clipMaskData = 1; mWebgl->UniformData(LOCAL_GL_INT, mImageProgramClipMask, false, {(const uint8_t*)&clipMaskData, sizeof(clipMaskData)}); } return true; } void DrawTargetWebgl::SharedContext::EnableScissor(const IntRect& aRect) { // Only update scissor state if it actually changes. if (!mLastScissor.IsEqualEdges(aRect)) { mLastScissor = aRect; mWebgl->Scissor(aRect.x, aRect.y, aRect.width, aRect.height); } if (!mScissorEnabled) { mScissorEnabled = true; mWebgl->Enable(LOCAL_GL_SCISSOR_TEST); } } void DrawTargetWebgl::SharedContext::DisableScissor() { if (mScissorEnabled) { mScissorEnabled = false; mWebgl->Disable(LOCAL_GL_SCISSOR_TEST); } } inline ColorPattern DrawTargetWebgl::GetClearPattern() const { return ColorPattern( DeviceColor(0.0f, 0.0f, 0.0f, IsOpaque(mFormat) ? 1.0f : 0.0f)); } // Check if the transformed rect would contain the entire viewport. inline bool DrawTargetWebgl::RectContainsViewport(const Rect& aRect) const { return mTransform.PreservesAxisAlignedRectangles() && MatrixDouble(mTransform) .TransformBounds( RectDouble(aRect.x, aRect.y, aRect.width, aRect.height)) .Contains(RectDouble(GetRect())); } // Ensure that the rect, after transform, is within reasonable precision limits // such that when transformed and clipped in the shader it will not round bits // from the mantissa in a way that will diverge in a noticeable way from path // geometry calculated by the path fallback. static inline bool RectInsidePrecisionLimits(const Rect& aRect, const Matrix& aTransform) { return Rect(-(1 << 20), -(1 << 20), 2 << 20, 2 << 20) .Contains(aTransform.TransformBounds(aRect)); } void DrawTargetWebgl::ClearRect(const Rect& aRect) { if (mIsClear) { // No need to clear anything if the entire framebuffer is already clear. return; } bool containsViewport = RectContainsViewport(aRect); if (containsViewport) { // If the rect encompasses the entire viewport, just clear the viewport // instead to avoid transform issues. DrawRect(Rect(GetRect()), GetClearPattern(), DrawOptions(1.0f, CompositionOp::OP_CLEAR), Nothing(), nullptr, false); } else if (RectInsidePrecisionLimits(aRect, mTransform)) { // If the rect transform won't stress precision, then just use it. DrawRect(aRect, GetClearPattern(), DrawOptions(1.0f, CompositionOp::OP_CLEAR)); } else { // Otherwise, using the transform in the shader may lead to inaccuracies, so // just fall back. MarkSkiaChanged(); mSkia->ClearRect(aRect); } // If the clear rectangle encompasses the entire viewport and is not clipped, // then mark the target as entirely clear. if (containsViewport && mSharedContext->IsCurrentTarget(this) && !mSharedContext->HasClipMask() && mSharedContext->mClipAARect.Contains(Rect(GetRect()))) { mIsClear = true; } } static inline DeviceColor PremultiplyColor(const DeviceColor& aColor, float aAlpha = 1.0f) { float a = aColor.a * aAlpha; return DeviceColor(aColor.r * a, aColor.g * a, aColor.b * a, a); } // Attempts to create the framebuffer used for drawing and also any relevant // non-shared resources. Returns whether or not this succeeded. bool DrawTargetWebgl::CreateFramebuffer() { RefPtr webgl = mSharedContext->mWebgl; if (!mFramebuffer) { mFramebuffer = webgl->CreateFramebuffer(); } if (!mTex) { mTex = webgl->CreateTexture(); webgl->BindTexture(LOCAL_GL_TEXTURE_2D, mTex); webgl->TexStorage2D(LOCAL_GL_TEXTURE_2D, 1, LOCAL_GL_RGBA8, mSize.width, mSize.height); mSharedContext->InitTexParameters(mTex); webgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mFramebuffer); webgl->FramebufferTexture2D(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0, LOCAL_GL_TEXTURE_2D, mTex, 0); webgl->Viewport(0, 0, mSize.width, mSize.height); mSharedContext->DisableScissor(); DeviceColor color = PremultiplyColor(GetClearPattern().mColor); webgl->ClearColor(color.b, color.g, color.r, color.a); webgl->Clear(LOCAL_GL_COLOR_BUFFER_BIT); mSharedContext->ClearTarget(); mSharedContext->ClearLastTexture(); } return true; } void DrawTargetWebgl::CopySurface(SourceSurface* aSurface, const IntRect& aSourceRect, const IntPoint& aDestination) { // Intersect the source and destination rectangles with the viewport bounds. IntRect destRect = IntRect(aDestination, aSourceRect.Size()).SafeIntersect(GetRect()); IntRect srcRect = destRect - aDestination + aSourceRect.TopLeft(); if (srcRect.IsEmpty()) { return; } if (mSkiaValid) { if (mSkiaLayer) { if (destRect.Contains(GetRect())) { // If the the destination would override the entire layer, discard the // layer. mSkiaLayer = false; } else if (!IsOpaque(aSurface->GetFormat())) { // If the surface is not opaque, copying it into the layer results in // unintended blending rather than a copy to the destination. FlattenSkia(); } } else { // If there is no layer, copying is safe. MarkSkiaChanged(); } mSkia->CopySurface(aSurface, srcRect, destRect.TopLeft()); return; } IntRect samplingRect; if (!mSharedContext->IsCompatibleSurface(aSurface)) { // If this data surface completely overwrites the framebuffer, then just // copy it to the Skia target. if (destRect.Contains(GetRect())) { MarkSkiaChanged(true); mSkia->DetachAllSnapshots(); mSkiaNoClip->CopySurface(aSurface, srcRect, destRect.TopLeft()); return; } // CopySurface usually only samples a surface once, so don't cache the // entire surface as it is unlikely to be reused. Limit it to the used // source rectangle instead. IntRect surfaceRect = aSurface->GetRect(); if (!srcRect.IsEqualEdges(surfaceRect)) { samplingRect = srcRect.SafeIntersect(surfaceRect); } } Matrix matrix = Matrix::Translation(destRect.TopLeft() - srcRect.TopLeft()); SurfacePattern pattern(aSurface, ExtendMode::CLAMP, matrix, SamplingFilter::POINT, samplingRect); DrawRect(Rect(destRect), pattern, DrawOptions(1.0f, CompositionOp::OP_SOURCE), Nothing(), nullptr, false, false); } void DrawTargetWebgl::PushClip(const Path* aPath) { if (aPath && aPath->GetBackendType() == BackendType::SKIA) { // Detect if the path is really just a rect to simplify caching. const PathSkia* pathSkia = static_cast(aPath); const SkPath& skPath = pathSkia->GetPath(); SkRect rect = SkRect::MakeEmpty(); if (skPath.isRect(&rect)) { PushClipRect(SkRectToRect(rect)); return; } } mClipChanged = true; mRefreshClipState = true; mSkia->PushClip(aPath); mClipStack.push_back({GetTransform(), Rect(), aPath}); } void DrawTargetWebgl::PushClipRect(const Rect& aRect) { mClipChanged = true; mRefreshClipState = true; mSkia->PushClipRect(aRect); mClipStack.push_back({GetTransform(), aRect, nullptr}); } void DrawTargetWebgl::PushDeviceSpaceClipRects(const IntRect* aRects, uint32_t aCount) { mClipChanged = true; mRefreshClipState = true; mSkia->PushDeviceSpaceClipRects(aRects, aCount); for (uint32_t i = 0; i < aCount; i++) { mClipStack.push_back({Matrix(), Rect(aRects[i]), nullptr}); } } void DrawTargetWebgl::PopClip() { mClipChanged = true; mRefreshClipState = true; mSkia->PopClip(); mClipStack.pop_back(); } bool DrawTargetWebgl::RemoveAllClips() { if (mClipStack.empty()) { return true; } if (!mSkia->RemoveAllClips()) { return false; } mClipChanged = true; mRefreshClipState = true; mClipStack.clear(); return true; } // Whether a given composition operator can be mapped to a WebGL blend mode. static inline bool SupportsDrawOptions(const DrawOptions& aOptions) { switch (aOptions.mCompositionOp) { case CompositionOp::OP_OVER: case CompositionOp::OP_ADD: case CompositionOp::OP_ATOP: case CompositionOp::OP_SOURCE: case CompositionOp::OP_CLEAR: return true; default: return false; } } // Whether a pattern can be mapped to an available WebGL shader. bool DrawTargetWebgl::SharedContext::SupportsPattern(const Pattern& aPattern) { switch (aPattern.GetType()) { case PatternType::COLOR: return true; case PatternType::SURFACE: { auto surfacePattern = static_cast(aPattern); if (surfacePattern.mExtendMode != ExtendMode::CLAMP) { return false; } if (surfacePattern.mSurface) { // If the surface is already uploaded to a texture, then just use it. if (IsCompatibleSurface(surfacePattern.mSurface)) { return true; } IntSize size = surfacePattern.mSurface->GetSize(); // The maximum size a surface can be before triggering a fallback to // software. Bound the maximum surface size by the actual texture size // limit. int32_t maxSize = int32_t( std::min(StaticPrefs::gfx_canvas_accelerated_max_surface_size(), mMaxTextureSize)); // Check if either of the surface dimensions or the sampling rect, // if supplied, exceed the maximum. if (std::max(size.width, size.height) > maxSize && (surfacePattern.mSamplingRect.IsEmpty() || std::max(surfacePattern.mSamplingRect.width, surfacePattern.mSamplingRect.height) > maxSize)) { return false; } } return true; } default: // Patterns other than colors and surfaces are currently not accelerated. return false; } } // Whether a given composition operator is associative and thus allows drawing // into a separate layer that can be later composited back into the WebGL // context. static inline bool SupportsLayering(const DrawOptions& aOptions) { switch (aOptions.mCompositionOp) { case CompositionOp::OP_OVER: // Layering is only supported for the default source-over composition op. return true; default: return false; } } // When a texture handle is no longer referenced, it must mark itself unused // by unlinking its owning surface. static void ReleaseTextureHandle(void* aPtr) { static_cast(aPtr)->SetSurface(nullptr); } bool DrawTargetWebgl::DrawRect(const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions, Maybe aMaskColor, RefPtr* aHandle, bool aTransformed, bool aClipped, bool aAccelOnly, bool aForceUpdate, const StrokeOptions* aStrokeOptions) { // If there is nothing to draw, then don't draw... if (aRect.IsEmpty()) { return true; } // If we're already drawing directly to the WebGL context, then we want to // continue to do so. However, if we're drawing into a Skia layer over the // WebGL context, then we need to be careful to avoid repeatedly clearing // and flushing the layer if we hit a drawing request that can be accelerated // in between layered drawing requests, as clearing and flushing the layer // can be significantly expensive when repeated. So when a Skia layer is // active, if it is possible to continue drawing into the layer, then don't // accelerate the drawing request. if (mWebglValid || (mSkiaLayer && !mLayerDepth && (aAccelOnly || !SupportsLayering(aOptions)))) { // If we get here, either the WebGL context is being directly drawn to // or we are going to flush the Skia layer to it before doing so. The shared // context still needs to be claimed and prepared for drawing. If this // fails, we just fall back to drawing with Skia below. if (PrepareContext(aClipped)) { // The shared context is claimed and the framebuffer is now valid, so try // accelerated drawing. return mSharedContext->DrawRectAccel( aRect, aPattern, aOptions, aMaskColor, aHandle, aTransformed, aClipped, aAccelOnly, aForceUpdate, aStrokeOptions); } } // Either there is no valid WebGL target to draw into, or we failed to prepare // it for drawing. The only thing we can do at this point is fall back to // drawing with Skia. If the request explicitly requires accelerated drawing, // then draw nothing before returning failure. if (!aAccelOnly) { DrawRectFallback(aRect, aPattern, aOptions, aMaskColor, aTransformed, aClipped, aStrokeOptions); } return false; } void DrawTargetWebgl::DrawRectFallback(const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions, Maybe aMaskColor, bool aTransformed, bool aClipped, const StrokeOptions* aStrokeOptions) { // Invalidate the WebGL target and prepare the Skia target for drawing. MarkSkiaChanged(aOptions); if (aTransformed) { // If transforms are requested, then just translate back to FillRect. if (aMaskColor) { mSkia->Mask(ColorPattern(*aMaskColor), aPattern, aOptions); } else if (aStrokeOptions) { mSkia->StrokeRect(aRect, aPattern, *aStrokeOptions, aOptions); } else { mSkia->FillRect(aRect, aPattern, aOptions); } } else if (aClipped) { // If no transform was requested but clipping is still required, then // temporarily reset the transform before translating to FillRect. mSkia->SetTransform(Matrix()); if (aMaskColor) { auto surfacePattern = static_cast(aPattern); if (surfacePattern.mSamplingRect.IsEmpty()) { mSkia->MaskSurface(ColorPattern(*aMaskColor), surfacePattern.mSurface, aRect.TopLeft(), aOptions); } else { mSkia->Mask(ColorPattern(*aMaskColor), aPattern, aOptions); } } else if (aStrokeOptions) { mSkia->StrokeRect(aRect, aPattern, *aStrokeOptions, aOptions); } else { mSkia->FillRect(aRect, aPattern, aOptions); } mSkia->SetTransform(mTransform); } else if (aPattern.GetType() == PatternType::SURFACE) { // No transform nor clipping was requested, so it is essentially just a // copy. auto surfacePattern = static_cast(aPattern); mSkia->CopySurface(surfacePattern.mSurface, surfacePattern.mSurface->GetRect(), IntPoint::Round(aRect.TopLeft())); } else { MOZ_ASSERT(false); } } inline already_AddRefed DrawTargetWebgl::SharedContext::GetCompatibleSnapshot( SourceSurface* aSurface) const { if (aSurface->GetType() == SurfaceType::WEBGL) { RefPtr webglSurf = static_cast(aSurface); if (this == webglSurf->mSharedContext) { // If there is a snapshot copy in a texture handle, use that. if (webglSurf->mHandle) { return do_AddRef(webglSurf->mHandle->GetWebGLTexture()); } if (RefPtr webglDT = webglSurf->GetTarget()) { // If there is a copy-on-write reference to a target, use its backing // texture directly. This is only safe if the targets don't match, but // MarkChanged should ensure that any snapshots were copied into a // texture handle before we ever get here. if (!IsCurrentTarget(webglDT)) { return do_AddRef(webglDT->mTex); } } } } return nullptr; } inline bool DrawTargetWebgl::SharedContext::IsCompatibleSurface( SourceSurface* aSurface) const { return bool(RefPtr(GetCompatibleSnapshot(aSurface))); } bool DrawTargetWebgl::SharedContext::UploadSurface( DataSourceSurface* aData, SurfaceFormat aFormat, const IntRect& aSrcRect, const IntPoint& aDstOffset, bool aInit, bool aZero, const RefPtr& aTex) { webgl::TexUnpackBlobDesc texDesc = { LOCAL_GL_TEXTURE_2D, {uint32_t(aSrcRect.width), uint32_t(aSrcRect.height), 1}}; if (aData) { // The surface needs to be uploaded to its backing texture either to // initialize or update the texture handle contents. Map the data // contents of the surface so it can be read. DataSourceSurface::ScopedMap map(aData, DataSourceSurface::READ); if (!map.IsMapped()) { return false; } int32_t stride = map.GetStride(); int32_t bpp = BytesPerPixel(aFormat); if (mCurrentTarget && mCurrentTarget->mShmem.IsWritable() && map.GetData() == mCurrentTarget->mShmem.get()) { texDesc.sd = Some(layers::SurfaceDescriptorBuffer( layers::RGBDescriptor(mCurrentTarget->mSize, SurfaceFormat::R8G8B8A8), mCurrentTarget->mShmem)); texDesc.structuredSrcSize = uvec2::From(stride / bpp, mCurrentTarget->mSize.height); texDesc.unpacking.skipPixels = aSrcRect.x; texDesc.unpacking.skipRows = aSrcRect.y; mWaitForShmem = true; } else { // Get the data pointer range considering the sampling rect offset and // size. Range range( map.GetData() + aSrcRect.y * size_t(stride) + aSrcRect.x * bpp, std::max(aSrcRect.height - 1, 0) * size_t(stride) + aSrcRect.width * bpp); texDesc.cpuData = Some(RawBuffer(range)); } // If the stride happens to be 4 byte aligned, assume that is the // desired alignment regardless of format (even A8). Otherwise, we // default to byte alignment. texDesc.unpacking.alignmentInTypeElems = stride % 4 ? 1 : 4; texDesc.unpacking.rowLength = stride / bpp; } else if (aZero) { // Create a PBO filled with zero data to initialize the texture data and // avoid slow initialization inside WebGL. MOZ_ASSERT(aSrcRect.TopLeft() == IntPoint(0, 0)); size_t size = size_t(GetAlignedStride<4>(aSrcRect.width, BytesPerPixel(aFormat))) * aSrcRect.height; if (!mZeroBuffer || size > mZeroSize) { mZeroBuffer = mWebgl->CreateBuffer(); mZeroSize = size; mWebgl->BindBuffer(LOCAL_GL_PIXEL_UNPACK_BUFFER, mZeroBuffer); // WebGL will zero initialize the empty buffer, so we don't send zero data // explicitly. mWebgl->RawBufferData(LOCAL_GL_PIXEL_UNPACK_BUFFER, nullptr, size, LOCAL_GL_STATIC_DRAW); } else { mWebgl->BindBuffer(LOCAL_GL_PIXEL_UNPACK_BUFFER, mZeroBuffer); } texDesc.pboOffset = Some(0); } // Upload as RGBA8 to avoid swizzling during upload. Surfaces provide // data as BGRA, but we manually swizzle that in the shader. An A8 // surface will be stored as an R8 texture that will also be swizzled // in the shader. GLenum intFormat = aFormat == SurfaceFormat::A8 ? LOCAL_GL_R8 : LOCAL_GL_RGBA8; GLenum extFormat = aFormat == SurfaceFormat::A8 ? LOCAL_GL_RED : LOCAL_GL_RGBA; webgl::PackingInfo texPI = {extFormat, LOCAL_GL_UNSIGNED_BYTE}; // Do the (partial) upload for the shared or standalone texture. if (aTex) { mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, aTex); } mWebgl->RawTexImage(0, aInit ? intFormat : 0, {uint32_t(aDstOffset.x), uint32_t(aDstOffset.y), 0}, texPI, std::move(texDesc)); if (aTex) { mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, mLastTexture); } if (!aData && aZero) { mWebgl->BindBuffer(LOCAL_GL_PIXEL_UNPACK_BUFFER, 0); } return true; } static inline SamplingFilter GetSamplingFilter(const Pattern& aPattern) { return aPattern.GetType() == PatternType::SURFACE ? static_cast(aPattern).mSamplingFilter : SamplingFilter::GOOD; } static inline bool UseNearestFilter(const Pattern& aPattern) { return GetSamplingFilter(aPattern) == SamplingFilter::POINT; } // Determine if the rectangle is still axis-aligned and pixel-aligned. static inline Maybe IsAlignedRect(bool aTransformed, const Matrix& aCurrentTransform, const Rect& aRect) { if (!aTransformed || aCurrentTransform.HasOnlyIntegerTranslation()) { auto intRect = RoundedToInt(aRect); if (aRect.WithinEpsilonOf(Rect(intRect), 1.0e-3f)) { if (aTransformed) { intRect += RoundedToInt(aCurrentTransform.GetTranslation()); } return Some(intRect); } } return Nothing(); } // Common rectangle and pattern drawing function shared by many DrawTarget // commands. If aMaskColor is specified, the provided surface pattern will be // treated as a mask. If aHandle is specified, then the surface pattern's // texture will be cached in the supplied handle, as opposed to using the // surface's user data. If aTransformed or aClipped are false, then transforms // and/or clipping will be disabled. If aAccelOnly is specified, then this // function will return before it would have otherwise drawn without // acceleration. If aForceUpdate is specified, then the provided texture handle // will be respecified with the provided surface. bool DrawTargetWebgl::SharedContext::DrawRectAccel( const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions, Maybe aMaskColor, RefPtr* aHandle, bool aTransformed, bool aClipped, bool aAccelOnly, bool aForceUpdate, const StrokeOptions* aStrokeOptions, const PathVertexRange* aVertexRange) { // If the rect or clip rect is empty, then there is nothing to draw. if (aRect.IsEmpty() || mClipRect.IsEmpty()) { return true; } // Check if the drawing options and the pattern support acceleration. Also // ensure the framebuffer is prepared for drawing. If not, fall back to using // the Skia target. if (!SupportsDrawOptions(aOptions) || !SupportsPattern(aPattern) || aStrokeOptions || !mCurrentTarget->MarkChanged()) { // If only accelerated drawing was requested, bail out without software // drawing fallback. if (!aAccelOnly) { MOZ_ASSERT(!aVertexRange); mCurrentTarget->DrawRectFallback(aRect, aPattern, aOptions, aMaskColor, aTransformed, aClipped, aStrokeOptions); } return false; } const Matrix& currentTransform = GetTransform(); if (aOptions.mCompositionOp == CompositionOp::OP_SOURCE && aTransformed && aClipped && (HasClipMask() || !currentTransform.PreservesAxisAlignedRectangles() || !currentTransform.TransformBounds(aRect).Contains(Rect(mClipAARect)) || (aPattern.GetType() == PatternType::SURFACE && !IsAlignedRect(aTransformed, currentTransform, aRect)))) { // Clear outside the mask region for masks that are not bounded by clip. return DrawRectAccel(Rect(mClipRect), ColorPattern(DeviceColor(0, 0, 0, 0)), DrawOptions(1.0f, CompositionOp::OP_SOURCE, aOptions.mAntialiasMode), Nothing(), nullptr, false, aClipped, aAccelOnly) && DrawRectAccel(aRect, aPattern, DrawOptions(aOptions.mAlpha, CompositionOp::OP_ADD, aOptions.mAntialiasMode), aMaskColor, aHandle, aTransformed, aClipped, aAccelOnly, aForceUpdate, aStrokeOptions, aVertexRange); } if (aOptions.mCompositionOp == CompositionOp::OP_CLEAR && aPattern.GetType() == PatternType::SURFACE && !aMaskColor) { // If the surface being drawn with clear is not a mask, then its contents // needs to be ignored. Just use a color pattern instead. return DrawRectAccel(aRect, ColorPattern(DeviceColor(1, 1, 1, 1)), aOptions, Nothing(), aHandle, aTransformed, aClipped, aAccelOnly, aForceUpdate, aStrokeOptions, aVertexRange); } // Set up the scissor test to reflect the clipping rectangle, if supplied. if (!mClipRect.Contains(IntRect(IntPoint(), mViewportSize))) { EnableScissor(mClipRect); } else { DisableScissor(); } bool success = false; // Now try to actually draw the pattern... switch (aPattern.GetType()) { case PatternType::COLOR: { if (!aVertexRange) { // Only an uncached draw if not using the vertex cache. mCurrentTarget->mProfile.OnUncachedDraw(); } DeviceColor color = PremultiplyColor( static_cast(aPattern).mColor, aOptions.mAlpha); if (((color.a == 1.0f && aOptions.mCompositionOp == CompositionOp::OP_OVER) || aOptions.mCompositionOp == CompositionOp::OP_SOURCE || aOptions.mCompositionOp == CompositionOp::OP_CLEAR) && !aStrokeOptions && !aVertexRange && !HasClipMask() && mClipAARect.IsEqualEdges(Rect(mClipRect))) { // Certain color patterns can be mapped to scissored clears. The // composition op must effectively overwrite the destination, and the // transform must map to an axis-aligned integer rectangle. if (Maybe intRect = IsAlignedRect(aTransformed, currentTransform, aRect)) { // Only use a clear if the area is larger than a quarter or the // viewport. if (intRect->Area() >= (mViewportSize.width / 2) * (mViewportSize.height / 2)) { if (!intRect->Contains(mClipRect)) { EnableScissor(intRect->Intersect(mClipRect)); } if (aOptions.mCompositionOp == CompositionOp::OP_CLEAR) { color = PremultiplyColor(mCurrentTarget->GetClearPattern().mColor); } mWebgl->ClearColor(color.b, color.g, color.r, color.a); mWebgl->Clear(LOCAL_GL_COLOR_BUFFER_BIT); success = true; break; } } } // Map the composition op to a WebGL blend mode, if possible. Maybe blendColor; if (aOptions.mCompositionOp == CompositionOp::OP_SOURCE || aOptions.mCompositionOp == CompositionOp::OP_CLEAR) { // The source operator can support clipping and AA by emulating it with // the over op. Supply the color with blend state, and set the shader // color to white, to avoid needing dual-source blending. blendColor = Some(color); // Both source and clear operators should output a mask from the shader. color = DeviceColor(1, 1, 1, 1); } SetBlendState(aOptions.mCompositionOp, blendColor); // Since it couldn't be mapped to a scissored clear, we need to use the // solid color shader with supplied transform. if (mLastProgram != mSolidProgram) { mWebgl->UseProgram(mSolidProgram); mLastProgram = mSolidProgram; // Ensure uniform state is current. mDirtyViewport = true; mDirtyAA = true; mDirtyClip = true; } if (mDirtyViewport) { float viewportData[2] = {float(mViewportSize.width), float(mViewportSize.height)}; mWebgl->UniformData( LOCAL_GL_FLOAT_VEC2, mSolidProgramViewport, false, {(const uint8_t*)viewportData, sizeof(viewportData)}); mDirtyViewport = false; } if (mDirtyAA || aVertexRange) { // Generated paths provide their own AA as vertex alpha. float aaData = aVertexRange ? 0.0f : 1.0f; mWebgl->UniformData(LOCAL_GL_FLOAT, mSolidProgramAA, false, {(const uint8_t*)&aaData, sizeof(aaData)}); mDirtyAA = aaData == 0.0f; } if (mDirtyClip) { // Offset the clip AA bounds by 0.5 to ensure AA falls to 0 at pixel // boundary. float clipData[4] = {mClipAARect.x - 0.5f, mClipAARect.y - 0.5f, mClipAARect.XMost() + 0.5f, mClipAARect.YMost() + 0.5f}; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC4, mSolidProgramClipBounds, false, {(const uint8_t*)clipData, sizeof(clipData)}); mDirtyClip = false; } float colorData[4] = {color.b, color.g, color.r, color.a}; Matrix xform(aRect.width, 0.0f, 0.0f, aRect.height, aRect.x, aRect.y); if (aTransformed) { xform *= currentTransform; } float xformData[6] = {xform._11, xform._12, xform._21, xform._22, xform._31, xform._32}; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC2, mSolidProgramTransform, false, {(const uint8_t*)xformData, sizeof(xformData)}); mWebgl->UniformData(LOCAL_GL_FLOAT_VEC4, mSolidProgramColor, false, {(const uint8_t*)colorData, sizeof(colorData)}); // Finally draw the colored rectangle. if (aVertexRange) { // If there's a vertex range, then we need to draw triangles within from // generated from a path stored in the path vertex buffer. mWebgl->DrawArrays(LOCAL_GL_TRIANGLES, GLint(aVertexRange->mOffset), GLsizei(aVertexRange->mLength)); } else { // Otherwise we're drawing a simple filled rectangle. mWebgl->DrawArrays(LOCAL_GL_TRIANGLE_FAN, 0, 4); } success = true; break; } case PatternType::SURFACE: { auto surfacePattern = static_cast(aPattern); // If a texture handle was supplied, or if the surface already has an // assigned texture handle stashed in its used data, try to use it. RefPtr handle = aHandle ? aHandle->get() : (surfacePattern.mSurface ? static_cast( surfacePattern.mSurface->GetUserData( &mTextureHandleKey)) : nullptr); IntSize texSize; IntPoint offset; SurfaceFormat format; // Check if the found handle is still valid and if its sampling rect // matches the requested sampling rect. if (handle && handle->IsValid() && (surfacePattern.mSamplingRect.IsEmpty() || handle->GetSamplingRect().IsEqualEdges( surfacePattern.mSamplingRect))) { texSize = handle->GetSize(); format = handle->GetFormat(); offset = handle->GetSamplingOffset(); } else { // Otherwise, there is no handle that can be used yet, so extract // information from the surface pattern. handle = nullptr; if (!surfacePattern.mSurface) { // If there was no actual surface supplied, then we tried to draw // using a texture handle, but the texture handle wasn't valid. break; } texSize = surfacePattern.mSurface->GetSize(); format = surfacePattern.mSurface->GetFormat(); if (!surfacePattern.mSamplingRect.IsEmpty()) { texSize = surfacePattern.mSamplingRect.Size(); offset = surfacePattern.mSamplingRect.TopLeft(); } } // We need to be able to transform from local space into texture space. Matrix invMatrix = surfacePattern.mMatrix; // If drawing a pre-transformed vertex range, then we need to ensure the // user-space pattern is still transformed to screen-space. if (aVertexRange && !aTransformed) { invMatrix *= currentTransform; } if (!invMatrix.Invert()) { break; } RefPtr tex; IntRect bounds; IntSize backingSize; RefPtr data; bool init = false; if (handle) { if (aForceUpdate) { data = surfacePattern.mSurface->GetDataSurface(); if (!data) { break; } // The size of the texture may change if we update contents. mUsedTextureMemory -= handle->UsedBytes(); handle->UpdateSize(texSize); mUsedTextureMemory += handle->UsedBytes(); handle->SetSamplingOffset(surfacePattern.mSamplingRect.TopLeft()); } // If using an existing handle, move it to the front of the MRU list. handle->remove(); mTextureHandles.insertFront(handle); } else if ((tex = GetCompatibleSnapshot(surfacePattern.mSurface))) { backingSize = surfacePattern.mSurface->GetSize(); bounds = IntRect(offset, texSize); // Count reusing a snapshot texture (no readback) as a cache hit. mCurrentTarget->mProfile.OnCacheHit(); } else { // If we get here, we need a data surface for a texture upload. data = surfacePattern.mSurface->GetDataSurface(); if (!data) { break; } // There is no existing handle. Calculate the bytes that would be used // by this texture, and prune enough other textures to ensure we have // that much usable texture space available to allocate. size_t usedBytes = TextureHandle::UsedBytes(format, texSize); PruneTextureMemory(usedBytes, false); // The requested page size for shared textures. int32_t pageSize = int32_t( std::min(StaticPrefs::gfx_canvas_accelerated_shared_page_size(), mMaxTextureSize)); if (!aForceUpdate && std::max(texSize.width, texSize.height) <= pageSize / 2) { // Ensure that the surface size won't change via forced update and // that the surface is no bigger than a quadrant of a shared texture // page. If so, try to allocate it to a shared texture. Look for any // existing shared texture page with a matching format and allocate // from that if possible. for (auto& shared : mSharedTextures) { if (shared->GetFormat() == format) { bool wasEmpty = !shared->HasAllocatedHandles(); handle = shared->Allocate(texSize); if (handle) { if (wasEmpty) { // If the page was previously empty, then deduct it from the // empty memory reserves. mEmptyTextureMemory -= shared->UsedBytes(); } break; } } } // If we couldn't find an existing shared texture page with matching // format, then allocate a new page to put the request in. if (!handle) { tex = mWebgl->CreateTexture(); if (!tex) { MOZ_ASSERT(false); break; } RefPtr shared = new SharedTexture(IntSize(pageSize, pageSize), format, tex); mSharedTextures.push_back(shared); mTotalTextureMemory += shared->UsedBytes(); handle = shared->Allocate(texSize); if (!handle) { MOZ_ASSERT(false); break; } init = true; } } else { // The surface wouldn't fit in a shared texture page, so we need to // allocate a standalone texture for it instead. tex = mWebgl->CreateTexture(); if (!tex) { MOZ_ASSERT(false); break; } RefPtr standalone = new StandaloneTexture(texSize, format, tex); mStandaloneTextures.push_back(standalone); mTotalTextureMemory += standalone->UsedBytes(); handle = standalone; init = true; } // Insert the new texture handle into the front of the MRU list and // update used space for it. mTextureHandles.insertFront(handle); ++mNumTextureHandles; mUsedTextureMemory += handle->UsedBytes(); // Link the handle to the surface's user data. handle->SetSamplingOffset(surfacePattern.mSamplingRect.TopLeft()); if (aHandle) { *aHandle = handle; } else { handle->SetSurface(surfacePattern.mSurface); surfacePattern.mSurface->AddUserData(&mTextureHandleKey, handle.get(), ReleaseTextureHandle); } } // Map the composition op to a WebGL blend mode, if possible. If there is // a mask color and a texture with multiple channels, assume subpixel // blending. If we encounter the source op here, then assume the surface // is opaque (non-opaque is handled above) and emulate it with over. SetBlendState(aOptions.mCompositionOp, format != SurfaceFormat::A8 ? aMaskColor : Nothing()); // Switch to the image shader and set up relevant transforms. if (mLastProgram != mImageProgram) { mWebgl->UseProgram(mImageProgram); mLastProgram = mImageProgram; // Ensure uniform state is current. mDirtyViewport = true; mDirtyAA = true; mDirtyClip = true; } if (mDirtyViewport) { float viewportData[2] = {float(mViewportSize.width), float(mViewportSize.height)}; mWebgl->UniformData( LOCAL_GL_FLOAT_VEC2, mImageProgramViewport, false, {(const uint8_t*)viewportData, sizeof(viewportData)}); mDirtyViewport = false; } if (mDirtyAA || aVertexRange) { // AA is not supported for OP_SOURCE. Generated paths provide their own // AA as vertex alpha. float aaData = mLastCompositionOp == CompositionOp::OP_SOURCE || aVertexRange ? 0.0f : 1.0f; mWebgl->UniformData(LOCAL_GL_FLOAT, mImageProgramAA, false, {(const uint8_t*)&aaData, sizeof(aaData)}); mDirtyAA = aaData == 0.0f; } if (mDirtyClip) { // Offset the clip AA bounds by 0.5 to ensure AA falls to 0 at pixel // boundary. float clipData[4] = {mClipAARect.x - 0.5f, mClipAARect.y - 0.5f, mClipAARect.XMost() + 0.5f, mClipAARect.YMost() + 0.5f}; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC4, mImageProgramClipBounds, false, {(const uint8_t*)clipData, sizeof(clipData)}); mDirtyClip = false; } DeviceColor color = mLastCompositionOp == CompositionOp::OP_CLEAR ? DeviceColor(1, 1, 1, 1) : PremultiplyColor( aMaskColor && format != SurfaceFormat::A8 ? DeviceColor::Mask(1.0f, aMaskColor->a) : aMaskColor.valueOr(DeviceColor(1, 1, 1, 1)), aOptions.mAlpha); float colorData[4] = {color.b, color.g, color.r, color.a}; float swizzleData = format == SurfaceFormat::A8 ? 1.0f : 0.0f; Matrix xform(aRect.width, 0.0f, 0.0f, aRect.height, aRect.x, aRect.y); if (aTransformed) { xform *= currentTransform; } float xformData[6] = {xform._11, xform._12, xform._21, xform._22, xform._31, xform._32}; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC2, mImageProgramTransform, false, {(const uint8_t*)xformData, sizeof(xformData)}); mWebgl->UniformData(LOCAL_GL_FLOAT_VEC4, mImageProgramColor, false, {(const uint8_t*)colorData, sizeof(colorData)}); mWebgl->UniformData(LOCAL_GL_FLOAT, mImageProgramSwizzle, false, {(const uint8_t*)&swizzleData, sizeof(swizzleData)}); // Start binding the WebGL state for the texture. if (handle) { if (!tex) { tex = handle->GetWebGLTexture(); } bounds = handle->GetBounds(); backingSize = handle->GetBackingSize(); } if (mLastTexture != tex) { mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, tex); mLastTexture = tex; } if (init) { // If this is the first time the texture is used, we need to initialize // the clamping and filtering state. InitTexParameters(tex); if (texSize != backingSize) { // If this is a shared texture handle whose actual backing texture is // larger than it, then we need to allocate the texture page to the // full backing size before we can do a partial upload of the surface. UploadSurface(nullptr, format, IntRect(IntPoint(), backingSize), IntPoint(), true, true); } } if (data) { UploadSurface(data, format, IntRect(offset, texSize), bounds.TopLeft(), texSize == backingSize); // Signal that we had to upload new data to the texture cache. mCurrentTarget->mProfile.OnCacheMiss(); } else { // Signal that we are reusing data from the texture cache. mCurrentTarget->mProfile.OnCacheHit(); } // Set up the texture coordinate matrix to map from the input rectangle to // the backing texture subrect. Size backingSizeF(backingSize); Matrix uvMatrix(aRect.width, 0.0f, 0.0f, aRect.height, aRect.x, aRect.y); uvMatrix *= invMatrix; uvMatrix *= Matrix(1.0f / backingSizeF.width, 0.0f, 0.0f, 1.0f / backingSizeF.height, float(bounds.x - offset.x) / backingSizeF.width, float(bounds.y - offset.y) / backingSizeF.height); float uvData[6] = {uvMatrix._11, uvMatrix._12, uvMatrix._21, uvMatrix._22, uvMatrix._31, uvMatrix._32}; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC2, mImageProgramTexMatrix, false, {(const uint8_t*)uvData, sizeof(uvData)}); // Clamp sampling to within the bounds of the backing texture subrect. float texBounds[4] = { (bounds.x + 0.5f) / backingSizeF.width, (bounds.y + 0.5f) / backingSizeF.height, (bounds.XMost() - 0.5f) / backingSizeF.width, (bounds.YMost() - 0.5f) / backingSizeF.height, }; mWebgl->UniformData(LOCAL_GL_FLOAT_VEC4, mImageProgramTexBounds, false, {(const uint8_t*)texBounds, sizeof(texBounds)}); // Ensure we use nearest filtering when no antialiasing is requested. if (UseNearestFilter(surfacePattern)) { SetTexFilter(tex, false); } // Finally draw the image rectangle. if (aVertexRange) { // If there's a vertex range, then we need to draw triangles within from // generated from a path stored in the path vertex buffer. mWebgl->DrawArrays(LOCAL_GL_TRIANGLES, GLint(aVertexRange->mOffset), GLsizei(aVertexRange->mLength)); } else { // Otherwise we're drawing a simple filled rectangle. mWebgl->DrawArrays(LOCAL_GL_TRIANGLE_FAN, 0, 4); } // Restore the default linear filter if overridden. if (UseNearestFilter(surfacePattern)) { SetTexFilter(tex, true); } success = true; break; } default: gfxWarning() << "Unknown DrawTargetWebgl::DrawRect pattern type: " << (int)aPattern.GetType(); break; } // mWebgl->Disable(LOCAL_GL_BLEND); return success; } bool DrawTargetWebgl::SharedContext::RemoveSharedTexture( const RefPtr& aTexture) { auto pos = std::find(mSharedTextures.begin(), mSharedTextures.end(), aTexture); if (pos == mSharedTextures.end()) { return false; } // Keep around a reserve of empty pages to avoid initialization costs from // allocating shared pages. If still below the limit of reserved pages, then // just add it to the reserve. Otherwise, erase the empty texture page. size_t maxBytes = StaticPrefs::gfx_canvas_accelerated_reserve_empty_cache() << 20; size_t usedBytes = aTexture->UsedBytes(); if (mEmptyTextureMemory + usedBytes <= maxBytes) { mEmptyTextureMemory += usedBytes; } else { mTotalTextureMemory -= usedBytes; mSharedTextures.erase(pos); ClearLastTexture(); mWebgl->DeleteTexture(aTexture->GetWebGLTexture()); } return true; } void SharedTextureHandle::Cleanup(DrawTargetWebgl::SharedContext& aContext) { mTexture->Free(*this); // Check if the shared handle's owning page has no more allocated handles // after we freed it. If so, remove the empty shared texture page also. if (!mTexture->HasAllocatedHandles()) { aContext.RemoveSharedTexture(mTexture); } } bool DrawTargetWebgl::SharedContext::RemoveStandaloneTexture( const RefPtr& aTexture) { auto pos = std::find(mStandaloneTextures.begin(), mStandaloneTextures.end(), aTexture); if (pos == mStandaloneTextures.end()) { return false; } mTotalTextureMemory -= aTexture->UsedBytes(); mStandaloneTextures.erase(pos); ClearLastTexture(); mWebgl->DeleteTexture(aTexture->GetWebGLTexture()); return true; } void StandaloneTexture::Cleanup(DrawTargetWebgl::SharedContext& aContext) { aContext.RemoveStandaloneTexture(this); } // Prune a given texture handle and release its associated resources. void DrawTargetWebgl::SharedContext::PruneTextureHandle( const RefPtr& aHandle) { // Invalidate the handle so nothing will subsequently use its contents. aHandle->Invalidate(); // If the handle has an associated SourceSurface, unlink it. UnlinkSurfaceTexture(aHandle); // If the handle has an associated CacheEntry, unlink it. if (RefPtr entry = aHandle->GetCacheEntry()) { entry->Unlink(); } // Deduct the used space from the total. mUsedTextureMemory -= aHandle->UsedBytes(); // Ensure any allocated shared or standalone texture regions get freed. aHandle->Cleanup(*this); } // Prune any texture memory above the limit (or margin below the limit) or any // least-recently-used handles that are no longer associated with any usable // surface. bool DrawTargetWebgl::SharedContext::PruneTextureMemory(size_t aMargin, bool aPruneUnused) { // The maximum amount of texture memory that may be used by textures. size_t maxBytes = StaticPrefs::gfx_canvas_accelerated_cache_size() << 20; maxBytes -= std::min(maxBytes, aMargin); size_t maxItems = StaticPrefs::gfx_canvas_accelerated_cache_items(); size_t oldItems = mNumTextureHandles; while (!mTextureHandles.isEmpty() && (mUsedTextureMemory > maxBytes || mNumTextureHandles > maxItems || (aPruneUnused && !mTextureHandles.getLast()->IsUsed()))) { PruneTextureHandle(mTextureHandles.popLast()); --mNumTextureHandles; } return mNumTextureHandles < oldItems; } void DrawTargetWebgl::FillRect(const Rect& aRect, const Pattern& aPattern, const DrawOptions& aOptions) { if (SupportsPattern(aPattern)) { if (RectInsidePrecisionLimits(aRect, mTransform)) { DrawRect(aRect, aPattern, aOptions); return; } if (aPattern.GetType() == PatternType::COLOR && RectContainsViewport(aRect)) { // If the pattern is transform-invariant and the rect encompasses the // entire viewport, just clip drawing to the viewport to avoid transform // issues. DrawRect(Rect(GetRect()), aPattern, aOptions, Nothing(), nullptr, false); return; } } if (!mWebglValid) { MarkSkiaChanged(aOptions); mSkia->FillRect(aRect, aPattern, aOptions); } else { // If the pattern is unsupported, then transform the rect to a path so it // can be cached. SkPath skiaPath; skiaPath.addRect(RectToSkRect(aRect)); RefPtr path = new PathSkia(skiaPath, FillRule::FILL_WINDING); DrawPath(path, aPattern, aOptions); } } void CacheEntry::Link(const RefPtr& aHandle) { mHandle = aHandle; mHandle->SetCacheEntry(this); } // When the CacheEntry becomes unused, it marks the corresponding // TextureHandle as unused and unlinks it from the CacheEntry. The // entry is removed from its containing Cache, if applicable. void CacheEntry::Unlink() { // The entry may not have a valid handle if rasterization failed. if (mHandle) { mHandle->SetCacheEntry(nullptr); mHandle = nullptr; } RemoveFromList(); } // Hashes a path and pattern to a single hash value that can be used for quick // comparisons. This currently avoids to expensive hashing of internal path // and pattern data for speed, relying instead on later exact comparisons for // disambiguation. HashNumber PathCacheEntry::HashPath(const QuantizedPath& aPath, const Pattern* aPattern, const Matrix& aTransform, const IntRect& aBounds, const Point& aOrigin) { HashNumber hash = 0; hash = AddToHash(hash, aPath.mPath.num_types); hash = AddToHash(hash, aPath.mPath.num_points); // Quantize the relative offset of the path to its bounds. IntPoint offset = RoundedToInt((aOrigin - Point(aBounds.TopLeft())) * 16.0f); hash = AddToHash(hash, offset.x); hash = AddToHash(hash, offset.y); hash = AddToHash(hash, aBounds.width); hash = AddToHash(hash, aBounds.height); if (aPattern) { hash = AddToHash(hash, (int)aPattern->GetType()); } return hash; } // When caching rendered geometry, we need to ensure the scale and orientation // is approximately the same. The offset will be considered separately. static inline bool HasMatchingScale(const Matrix& aTransform1, const Matrix& aTransform2) { return FuzzyEqual(aTransform1._11, aTransform2._11) && FuzzyEqual(aTransform1._12, aTransform2._12) && FuzzyEqual(aTransform1._21, aTransform2._21) && FuzzyEqual(aTransform1._22, aTransform2._22); } // Determines if an existing path cache entry matches an incoming path and // pattern. inline bool PathCacheEntry::MatchesPath(const QuantizedPath& aPath, const Pattern* aPattern, const StrokeOptions* aStrokeOptions, const Matrix& aTransform, const IntRect& aBounds, const Point& aOrigin, HashNumber aHash, float aSigma) { return aHash == mHash && HasMatchingScale(aTransform, mTransform) && // Ensure the clipped relative bounds fit inside those of the entry aBounds.x - aOrigin.x >= mBounds.x - mOrigin.x && (aBounds.x - aOrigin.x) + aBounds.width <= (mBounds.x - mOrigin.x) + mBounds.width && aBounds.y - aOrigin.y >= mBounds.y - mOrigin.y && (aBounds.y - aOrigin.y) + aBounds.height <= (mBounds.y - mOrigin.y) + mBounds.height && aPath == mPath && (!aPattern ? !mPattern : mPattern && *aPattern == *mPattern) && (!aStrokeOptions ? !mStrokeOptions : mStrokeOptions && *aStrokeOptions == *mStrokeOptions) && aSigma == mSigma; } PathCacheEntry::PathCacheEntry(QuantizedPath&& aPath, Pattern* aPattern, StoredStrokeOptions* aStrokeOptions, const Matrix& aTransform, const IntRect& aBounds, const Point& aOrigin, HashNumber aHash, float aSigma) : CacheEntryImpl(aTransform, aBounds, aHash), mPath(std::move(aPath)), mOrigin(aOrigin), mPattern(aPattern), mStrokeOptions(aStrokeOptions), mSigma(aSigma) {} // Attempt to find a matching entry in the path cache. If one isn't found, // a new entry will be created. The caller should check whether the contained // texture handle is valid to determine if it will need to render the text run // or just reuse the cached texture. already_AddRefed PathCache::FindOrInsertEntry( QuantizedPath aPath, const Pattern* aPattern, const StrokeOptions* aStrokeOptions, const Matrix& aTransform, const IntRect& aBounds, const Point& aOrigin, float aSigma) { HashNumber hash = PathCacheEntry::HashPath(aPath, aPattern, aTransform, aBounds, aOrigin); for (const RefPtr& entry : GetChain(hash)) { if (entry->MatchesPath(aPath, aPattern, aStrokeOptions, aTransform, aBounds, aOrigin, hash, aSigma)) { return do_AddRef(entry); } } Pattern* pattern = nullptr; if (aPattern) { pattern = aPattern->CloneWeak(); if (!pattern) { return nullptr; } } StoredStrokeOptions* strokeOptions = nullptr; if (aStrokeOptions) { strokeOptions = aStrokeOptions->Clone(); if (!strokeOptions) { return nullptr; } } RefPtr entry = new PathCacheEntry(std::move(aPath), pattern, strokeOptions, aTransform, aBounds, aOrigin, hash, aSigma); Insert(entry); return entry.forget(); } void DrawTargetWebgl::Fill(const Path* aPath, const Pattern& aPattern, const DrawOptions& aOptions) { if (!aPath || aPath->GetBackendType() != BackendType::SKIA) { return; } const SkPath& skiaPath = static_cast(aPath)->GetPath(); SkRect skiaRect = SkRect::MakeEmpty(); // Draw the path as a simple rectangle with a supported pattern when possible. if (skiaPath.isRect(&skiaRect) && SupportsPattern(aPattern)) { Rect rect = SkRectToRect(skiaRect); if (RectInsidePrecisionLimits(rect, mTransform)) { DrawRect(rect, aPattern, aOptions); return; } if (aPattern.GetType() == PatternType::COLOR && RectContainsViewport(rect)) { // If the pattern is transform-invariant and the rect encompasses the // entire viewport, just clip drawing to the viewport to avoid transform // issues. DrawRect(Rect(GetRect()), aPattern, aOptions, Nothing(), nullptr, false); return; } } DrawPath(aPath, aPattern, aOptions); } QuantizedPath::QuantizedPath(const WGR::Path& aPath) : mPath(aPath) {} QuantizedPath::QuantizedPath(QuantizedPath&& aPath) noexcept : mPath(aPath.mPath) { aPath.mPath.points = nullptr; aPath.mPath.num_points = 0; aPath.mPath.types = nullptr; aPath.mPath.num_types = 0; } QuantizedPath::~QuantizedPath() { if (mPath.points || mPath.types) { WGR::wgr_path_release(mPath); } } bool QuantizedPath::operator==(const QuantizedPath& aOther) const { return mPath.num_types == aOther.mPath.num_types && mPath.num_points == aOther.mPath.num_points && mPath.fill_mode == aOther.mPath.fill_mode && !memcmp(mPath.types, aOther.mPath.types, mPath.num_types * sizeof(uint8_t)) && !memcmp(mPath.points, aOther.mPath.points, mPath.num_points * sizeof(WGR::Point)); } // Generate a quantized path from the Skia path using WGR. The supplied // transform will be applied to the path. The path is stored relative to its // bounds origin to support translation later. static Maybe GenerateQuantizedPath(const SkPath& aPath, const Rect& aBounds, const Matrix& aTransform) { WGR::PathBuilder* pb = WGR::wgr_new_builder(); if (!pb) { return Nothing(); } WGR::wgr_builder_set_fill_mode(pb, aPath.getFillType() == SkPathFillType::kWinding ? WGR::FillMode::Winding : WGR::FillMode::EvenOdd); SkPath::RawIter iter(aPath); SkPoint params[4]; SkPath::Verb currentVerb; // printf_stderr("bounds: (%d, %d) %d x %d\n", aBounds.x, aBounds.y, // aBounds.width, aBounds.height); Matrix transform = aTransform; transform.PostTranslate(-aBounds.TopLeft()); while ((currentVerb = iter.next(params)) != SkPath::kDone_Verb) { switch (currentVerb) { case SkPath::kMove_Verb: { Point p0 = transform.TransformPoint(SkPointToPoint(params[0])); WGR::wgr_builder_move_to(pb, p0.x, p0.y); break; } case SkPath::kLine_Verb: { Point p1 = transform.TransformPoint(SkPointToPoint(params[1])); WGR::wgr_builder_line_to(pb, p1.x, p1.y); break; } case SkPath::kCubic_Verb: { Point p1 = transform.TransformPoint(SkPointToPoint(params[1])); Point p2 = transform.TransformPoint(SkPointToPoint(params[2])); Point p3 = transform.TransformPoint(SkPointToPoint(params[3])); // printf_stderr("cubic (%f, %f), (%f, %f), (%f, %f)\n", p1.x, p1.y, // p2.x, p2.y, p3.x, p3.y); WGR::wgr_builder_curve_to(pb, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y); break; } case SkPath::kQuad_Verb: { Point p1 = transform.TransformPoint(SkPointToPoint(params[1])); Point p2 = transform.TransformPoint(SkPointToPoint(params[2])); // printf_stderr("quad (%f, %f), (%f, %f)\n", p1.x, p1.y, p2.x, p2.y); WGR::wgr_builder_quad_to(pb, p1.x, p1.y, p2.x, p2.y); break; } case SkPath::kConic_Verb: { Point p0 = transform.TransformPoint(SkPointToPoint(params[0])); Point p1 = transform.TransformPoint(SkPointToPoint(params[1])); Point p2 = transform.TransformPoint(SkPointToPoint(params[2])); float w = iter.conicWeight(); std::vector quads; int numQuads = ConvertConicToQuads(p0, p1, p2, w, quads); for (int i = 0; i < numQuads; i++) { Point q1 = quads[2 * i + 1]; Point q2 = quads[2 * i + 2]; // printf_stderr("conic quad (%f, %f), (%f, %f)\n", q1.x, q1.y, q2.x, // q2.y); WGR::wgr_builder_quad_to(pb, q1.x, q1.y, q2.x, q2.y); } break; } case SkPath::kClose_Verb: // printf_stderr("close\n"); WGR::wgr_builder_close(pb); break; default: MOZ_ASSERT(false); // Unexpected verb found in path! WGR::wgr_builder_release(pb); return Nothing(); } } WGR::Path p = WGR::wgr_builder_get_path(pb); WGR::wgr_builder_release(pb); if (!p.num_points || !p.num_types) { WGR::wgr_path_release(p); return Nothing(); } return Some(QuantizedPath(p)); } // Get the output vertex buffer using WGR from an input quantized path. static Maybe GeneratePathVertexBuffer( const QuantizedPath& aPath, const IntRect& aClipRect, bool aRasterizationTruncates, WGR::OutputVertex* aBuffer, size_t aBufferCapacity) { WGR::VertexBuffer vb = WGR::wgr_path_rasterize_to_tri_list( &aPath.mPath, aClipRect.x, aClipRect.y, aClipRect.width, aClipRect.height, true, false, aRasterizationTruncates, aBuffer, aBufferCapacity); if (!vb.len || (aBuffer && vb.len > aBufferCapacity)) { WGR::wgr_vertex_buffer_release(vb); return Nothing(); } return Some(vb); } static inline AAStroke::LineJoin ToAAStrokeLineJoin(JoinStyle aJoin) { switch (aJoin) { case JoinStyle::BEVEL: return AAStroke::LineJoin::Bevel; case JoinStyle::ROUND: return AAStroke::LineJoin::Round; case JoinStyle::MITER: case JoinStyle::MITER_OR_BEVEL: return AAStroke::LineJoin::Miter; } return AAStroke::LineJoin::Miter; } static inline AAStroke::LineCap ToAAStrokeLineCap(CapStyle aCap) { switch (aCap) { case CapStyle::BUTT: return AAStroke::LineCap::Butt; case CapStyle::ROUND: return AAStroke::LineCap::Round; case CapStyle::SQUARE: return AAStroke::LineCap::Square; } return AAStroke::LineCap::Butt; } static inline Point WGRPointToPoint(const WGR::Point& aPoint) { return Point(IntPoint(aPoint.x, aPoint.y)) * (1.0f / 16.0f); } // Generates a vertex buffer for a stroked path using aa-stroke. static Maybe GenerateStrokeVertexBuffer( const QuantizedPath& aPath, const StrokeOptions* aStrokeOptions, float aScale, WGR::OutputVertex* aBuffer, size_t aBufferCapacity) { AAStroke::StrokeStyle style = {aStrokeOptions->mLineWidth * aScale, ToAAStrokeLineCap(aStrokeOptions->mLineCap), ToAAStrokeLineJoin(aStrokeOptions->mLineJoin), aStrokeOptions->mMiterLimit}; if (style.width <= 0.0f || !std::isfinite(style.width) || style.miter_limit <= 0.0f || !std::isfinite(style.miter_limit)) { return Nothing(); } AAStroke::Stroker* s = AAStroke::aa_stroke_new( &style, (AAStroke::OutputVertex*)aBuffer, aBufferCapacity); bool valid = true; size_t curPoint = 0; for (size_t curType = 0; valid && curType < aPath.mPath.num_types;) { // Verify that we are at the start of a sub-path. if ((aPath.mPath.types[curType] & WGR::PathPointTypePathTypeMask) != WGR::PathPointTypeStart) { valid = false; break; } // Find where the next sub-path starts so we can locate the end. size_t endType = curType + 1; for (; endType < aPath.mPath.num_types; endType++) { if ((aPath.mPath.types[endType] & WGR::PathPointTypePathTypeMask) == WGR::PathPointTypeStart) { break; } } // Check if the path is closed. This is a flag modifying the last type. bool closed = (aPath.mPath.types[endType - 1] & WGR::PathPointTypeCloseSubpath) != 0; for (; curType < endType; curType++) { // If this is the last type and the sub-path is not closed, determine if // this segment should be capped. bool end = curType + 1 == endType && !closed; switch (aPath.mPath.types[curType] & WGR::PathPointTypePathTypeMask) { case WGR::PathPointTypeStart: { if (curPoint + 1 > aPath.mPath.num_points) { valid = false; break; } Point p1 = WGRPointToPoint(aPath.mPath.points[curPoint]); AAStroke::aa_stroke_move_to(s, p1.x, p1.y, closed); if (end) { AAStroke::aa_stroke_line_to(s, p1.x, p1.y, true); } curPoint++; break; } case WGR::PathPointTypeLine: { if (curPoint + 1 > aPath.mPath.num_points) { valid = false; break; } Point p1 = WGRPointToPoint(aPath.mPath.points[curPoint]); AAStroke::aa_stroke_line_to(s, p1.x, p1.y, end); curPoint++; break; } case WGR::PathPointTypeBezier: { if (curPoint + 3 > aPath.mPath.num_points) { valid = false; break; } Point p1 = WGRPointToPoint(aPath.mPath.points[curPoint]); Point p2 = WGRPointToPoint(aPath.mPath.points[curPoint + 1]); Point p3 = WGRPointToPoint(aPath.mPath.points[curPoint + 2]); AAStroke::aa_stroke_curve_to(s, p1.x, p1.y, p2.x, p2.y, p3.x, p3.y, end); curPoint += 3; break; } default: MOZ_ASSERT(false, "Unknown WGR path point type"); valid = false; break; } } // Close the sub-path if necessary. if (valid && closed) { AAStroke::aa_stroke_close(s); } } Maybe result; if (valid) { AAStroke::VertexBuffer vb = AAStroke::aa_stroke_finish(s); if (!vb.len || (aBuffer && vb.len > aBufferCapacity)) { AAStroke::aa_stroke_vertex_buffer_release(vb); } else { result = Some(vb); } } AAStroke::aa_stroke_release(s); return result; } // Search the path cache for any entries stored in the path vertex buffer and // remove them. void PathCache::ClearVertexRanges() { for (auto& chain : mChains) { PathCacheEntry* entry = chain.getFirst(); while (entry) { PathCacheEntry* next = entry->getNext(); if (entry->GetVertexRange().IsValid()) { entry->Unlink(); } entry = next; } } } inline bool DrawTargetWebgl::ShouldAccelPath( const DrawOptions& aOptions, const StrokeOptions* aStrokeOptions) { return mWebglValid && SupportsDrawOptions(aOptions) && PrepareContext(); } // For now, we only support stroking solid color patterns to limit artifacts // from blending of overlapping geometry generated by AAStroke. static inline bool SupportsAAStroke(const Pattern& aPattern, const DrawOptions& aOptions, const StrokeOptions& aStrokeOptions) { if (aStrokeOptions.mDashPattern) { return false; } switch (aOptions.mCompositionOp) { case CompositionOp::OP_SOURCE: return true; case CompositionOp::OP_OVER: return aPattern.GetType() == PatternType::COLOR && static_cast(aPattern).mColor.a * aOptions.mAlpha == 1.0f; default: return false; } } bool DrawTargetWebgl::SharedContext::DrawPathAccel( const Path* aPath, const Pattern& aPattern, const DrawOptions& aOptions, const StrokeOptions* aStrokeOptions, const ShadowOptions* aShadow, bool aCacheable) { // Get the transformed bounds for the path and conservatively check if the // bounds overlap the canvas. const PathSkia* pathSkia = static_cast(aPath); const Matrix& currentTransform = GetTransform(); Rect bounds = pathSkia->GetFastBounds(currentTransform, aStrokeOptions); // If the path is empty, then there is nothing to draw. if (bounds.IsEmpty()) { return true; } IntRect viewport(IntPoint(), mViewportSize); if (aShadow) { // Inflate the bounds to account for the blur radius. bounds += aShadow->mOffset; int32_t blurRadius = aShadow->BlurRadius(); bounds.Inflate(blurRadius); viewport.Inflate(blurRadius); } Point realOrigin = bounds.TopLeft(); if (aCacheable) { // Quantize the path origin to increase the reuse of cache entries. bounds.Scale(4.0f); bounds.Round(); bounds.Scale(0.25f); } Point quantizedOrigin = bounds.TopLeft(); // If the path doesn't intersect the viewport, then there is nothing to draw. IntRect intBounds = RoundedOut(bounds).Intersect(viewport); if (intBounds.IsEmpty()) { return true; } // Nudge the bounds to account for the quantization rounding. Rect quantBounds = Rect(intBounds) + (realOrigin - quantizedOrigin); // If the pattern is a solid color, then this will be used along with a path // mask to render the path, as opposed to baking the pattern into the cached // path texture. Maybe color = aOptions.mCompositionOp == CompositionOp::OP_CLEAR ? Some(DeviceColor(1, 1, 1, 1)) : (aPattern.GetType() == PatternType::COLOR ? Some(static_cast(aPattern).mColor) : Nothing()); // Look for an existing path cache entry, if possible, or otherwise create // one. If the draw request is not cacheable, then don't create an entry. RefPtr entry; RefPtr handle; if (aCacheable) { if (!mPathCache) { mPathCache = MakeUnique(); } // Use a quantized, relative (to its bounds origin) version of the path as // a cache key to help limit cache bloat. Maybe qp = GenerateQuantizedPath( pathSkia->GetPath(), quantBounds, currentTransform); if (!qp) { return false; } entry = mPathCache->FindOrInsertEntry( std::move(*qp), color ? nullptr : &aPattern, aStrokeOptions, currentTransform, intBounds, quantizedOrigin, aShadow ? aShadow->mSigma : -1.0f); if (!entry) { return false; } handle = entry->GetHandle(); } // If there is a shadow, it needs to draw with the shadow color rather than // the path color. Maybe shadowColor = color; if (aShadow && aOptions.mCompositionOp != CompositionOp::OP_CLEAR) { shadowColor = Some(aShadow->mColor); if (color) { shadowColor->a *= color->a; } } SamplingFilter filter = aShadow ? SamplingFilter::GOOD : GetSamplingFilter(aPattern); if (handle && handle->IsValid()) { // If the entry has a valid texture handle still, use it. However, the // entry texture is assumed to be located relative to its previous bounds. // We need to offset the pattern by the difference between its new unclipped // origin and its previous previous unclipped origin. Then when we finally // draw a rectangle at the expected new bounds, it will overlap the portion // of the old entry texture we actually need to sample from. Point offset = (realOrigin - entry->GetOrigin()) + entry->GetBounds().TopLeft(); SurfacePattern pathPattern(nullptr, ExtendMode::CLAMP, Matrix::Translation(offset), filter); return DrawRectAccel(quantBounds, pathPattern, aOptions, shadowColor, &handle, false, true, true); } if (mPathVertexCapacity > 0 && !handle && entry && !aShadow && aOptions.mAntialiasMode != AntialiasMode::NONE && SupportsPattern(aPattern) && entry->GetPath().mPath.num_types <= mPathMaxComplexity) { if (entry->GetVertexRange().IsValid()) { // If there is a valid cached vertex data in the path vertex buffer, then // just draw that. We must draw at integer pixel boundaries (using // intBounds instead of quantBounds) due to WGR's reliance on pixel center // location. mCurrentTarget->mProfile.OnCacheHit(); return DrawRectAccel(Rect(intBounds.TopLeft(), Size(1, 1)), aPattern, aOptions, Nothing(), nullptr, false, true, true, false, nullptr, &entry->GetVertexRange()); } // printf_stderr("Generating... verbs %d, points %d\n", // int(pathSkia->GetPath().countVerbs()), // int(pathSkia->GetPath().countPoints())); WGR::OutputVertex* outputBuffer = nullptr; size_t outputBufferCapacity = 0; if (mWGROutputBuffer) { outputBuffer = mWGROutputBuffer.get(); outputBufferCapacity = mPathVertexCapacity / sizeof(WGR::OutputVertex); } Maybe wgrVB; Maybe strokeVB; if (!aStrokeOptions) { wgrVB = GeneratePathVertexBuffer( entry->GetPath(), IntRect(-intBounds.TopLeft(), mViewportSize), mRasterizationTruncates, outputBuffer, outputBufferCapacity); } else { if (mPathAAStroke && SupportsAAStroke(aPattern, aOptions, *aStrokeOptions)) { auto scaleFactors = currentTransform.ScaleFactors(); if (scaleFactors.AreScalesSame()) { strokeVB = GenerateStrokeVertexBuffer( entry->GetPath(), aStrokeOptions, scaleFactors.xScale, outputBuffer, outputBufferCapacity); } } if (!strokeVB && mPathWGRStroke) { // If stroking, then generate a path to fill the stroked region. This // path will need to be quantized again because it differs from the // path used for the cache entry, but this allows us to avoid // generating a fill path on a cache hit. Maybe cullRect; Matrix invTransform = currentTransform; if (invTransform.Invert()) { // Transform the stroking clip rect from device space to local // space. Rect invRect = invTransform.TransformBounds(Rect(mClipRect)); invRect.RoundOut(); cullRect = Some(invRect); } SkPath fillPath; if (pathSkia->GetFillPath(*aStrokeOptions, currentTransform, fillPath, cullRect)) { // printf_stderr(" stroke fill... verbs %d, points %d\n", // int(fillPath.countVerbs()), // int(fillPath.countPoints())); if (Maybe qp = GenerateQuantizedPath( fillPath, quantBounds, currentTransform)) { wgrVB = GeneratePathVertexBuffer( *qp, IntRect(-intBounds.TopLeft(), mViewportSize), mRasterizationTruncates, outputBuffer, outputBufferCapacity); } } } } if (wgrVB || strokeVB) { const uint8_t* vbData = wgrVB ? (const uint8_t*)wgrVB->data : (const uint8_t*)strokeVB->data; if (outputBuffer && !vbData) { vbData = (const uint8_t*)outputBuffer; } size_t vbLen = wgrVB ? wgrVB->len : strokeVB->len; uint32_t vertexBytes = uint32_t( std::min(vbLen * sizeof(WGR::OutputVertex), size_t(UINT32_MAX))); // printf_stderr(" ... %d verts, %d bytes\n", int(vbLen), // int(vertexBytes)); if (vertexBytes > mPathVertexCapacity - mPathVertexOffset && vertexBytes <= mPathVertexCapacity - sizeof(kRectVertexData)) { // If the vertex data is too large to fit in the remaining path vertex // buffer, then orphan the contents of the vertex buffer to make room // for it. if (mPathCache) { mPathCache->ClearVertexRanges(); } ResetPathVertexBuffer(false); } if (vertexBytes <= mPathVertexCapacity - mPathVertexOffset) { // If there is actually room to fit the vertex data in the vertex buffer // after orphaning as necessary, then upload the data to the next // available offset in the buffer. PathVertexRange vertexRange( uint32_t(mPathVertexOffset / sizeof(WGR::OutputVertex)), uint32_t(vbLen)); if (entry) { entry->SetVertexRange(vertexRange); } // printf_stderr(" ... offset %d\n", mPathVertexOffset); // Normal glBufferSubData interleaved with draw calls causes performance // issues on Mali, so use our special unsynchronized version. This is // safe as we never update regions referenced by pending draw calls. mWebgl->RawBufferSubData(LOCAL_GL_ARRAY_BUFFER, mPathVertexOffset, vbData, vertexBytes, /* unsynchronized */ true); mPathVertexOffset += vertexBytes; if (wgrVB) { WGR::wgr_vertex_buffer_release(wgrVB.ref()); } else { AAStroke::aa_stroke_vertex_buffer_release(strokeVB.ref()); } // Finally, draw the uploaded vertex data. mCurrentTarget->mProfile.OnCacheMiss(); return DrawRectAccel(Rect(intBounds.TopLeft(), Size(1, 1)), aPattern, aOptions, Nothing(), nullptr, false, true, true, false, nullptr, &vertexRange); } if (wgrVB) { WGR::wgr_vertex_buffer_release(wgrVB.ref()); } else { AAStroke::aa_stroke_vertex_buffer_release(strokeVB.ref()); } // If we failed to draw the vertex data for some reason, then fall through // to the texture rasterization path. } } // If a stroke path covers too much screen area, it is likely that most is // empty space in the interior. This usually imposes too high a cost versus // just rasterizing without acceleration. Note that AA-Stroke generally // produces more acceptable amounts of geometry for larger paths, so we do // this heuristic after we attempt AA-Stroke. if (aStrokeOptions && intBounds.width * intBounds.height > (mViewportSize.width / 2) * (mViewportSize.height / 2)) { return false; } // If there isn't a valid texture handle, then we need to rasterize the // path in a software canvas and upload this to a texture. Solid color // patterns will be rendered as a path mask that can then be modulated // with any color. Other pattern types have to rasterize the pattern // directly into the cached texture. handle = nullptr; RefPtr pathDT = new DrawTargetSkia; if (pathDT->Init(intBounds.Size(), color || aShadow ? SurfaceFormat::A8 : SurfaceFormat::B8G8R8A8)) { Point offset = -quantBounds.TopLeft(); if (aShadow) { // Ensure the the shadow is drawn at the requested offset offset += aShadow->mOffset; } pathDT->SetTransform(currentTransform * Matrix::Translation(offset)); DrawOptions drawOptions(1.0f, CompositionOp::OP_OVER, aOptions.mAntialiasMode); static const ColorPattern maskPattern(DeviceColor(1.0f, 1.0f, 1.0f, 1.0f)); const Pattern& cachePattern = color ? maskPattern : aPattern; // If the source pattern is a DrawTargetWebgl snapshot, we may shift // targets when drawing the path, so back up the old target. DrawTargetWebgl* oldTarget = mCurrentTarget; if (aStrokeOptions) { pathDT->Stroke(aPath, cachePattern, *aStrokeOptions, drawOptions); } else { pathDT->Fill(aPath, cachePattern, drawOptions); } if (aShadow && aShadow->mSigma > 0.0f) { // Blur the shadow if required. uint8_t* data = nullptr; IntSize size; int32_t stride = 0; SurfaceFormat format = SurfaceFormat::UNKNOWN; if (pathDT->LockBits(&data, &size, &stride, &format)) { AlphaBoxBlur blur(Rect(pathDT->GetRect()), stride, aShadow->mSigma, aShadow->mSigma); blur.Blur(data); pathDT->ReleaseBits(data); } } RefPtr pathSurface = pathDT->Snapshot(); if (pathSurface) { // If the target changed, try to restore it. if (mCurrentTarget != oldTarget && !oldTarget->PrepareContext()) { return false; } SurfacePattern pathPattern(pathSurface, ExtendMode::CLAMP, Matrix::Translation(quantBounds.TopLeft()), filter); // Try and upload the rasterized path to a texture. If there is a // valid texture handle after this, then link it to the entry. // Otherwise, we might have to fall back to software drawing the // path, so unlink it from the entry. if (DrawRectAccel(quantBounds, pathPattern, aOptions, shadowColor, &handle, false, true) && handle) { if (entry) { entry->Link(handle); } } else if (entry) { entry->Unlink(); } return true; } } return false; } void DrawTargetWebgl::DrawPath(const Path* aPath, const Pattern& aPattern, const DrawOptions& aOptions, const StrokeOptions* aStrokeOptions) { // If there is a WebGL context, then try to cache the path to avoid slow // fallbacks. if (ShouldAccelPath(aOptions, aStrokeOptions) && mSharedContext->DrawPathAccel(aPath, aPattern, aOptions, aStrokeOptions)) { return; } // There was no path cache entry available to use, so fall back to drawing the // path with Skia. MarkSkiaChanged(aOptions); if (aStrokeOptions) { mSkia->Stroke(aPath, aPattern, *aStrokeOptions, aOptions); } else { mSkia->Fill(aPath, aPattern, aOptions); } } void DrawTargetWebgl::DrawSurface(SourceSurface* aSurface, const Rect& aDest, const Rect& aSource, const DrawSurfaceOptions& aSurfOptions, const DrawOptions& aOptions) { Matrix matrix = Matrix::Scaling(aDest.width / aSource.width, aDest.height / aSource.height); matrix.PreTranslate(-aSource.x, -aSource.y); matrix.PostTranslate(aDest.x, aDest.y); SurfacePattern pattern(aSurface, ExtendMode::CLAMP, matrix, aSurfOptions.mSamplingFilter); DrawRect(aDest, pattern, aOptions); } void DrawTargetWebgl::Mask(const Pattern& aSource, const Pattern& aMask, const DrawOptions& aOptions) { if (!SupportsDrawOptions(aOptions) || aMask.GetType() != PatternType::SURFACE || aSource.GetType() != PatternType::COLOR) { MarkSkiaChanged(aOptions); mSkia->Mask(aSource, aMask, aOptions); return; } auto sourceColor = static_cast(aSource).mColor; auto maskPattern = static_cast(aMask); DrawRect(Rect(IntRect(IntPoint(), maskPattern.mSurface->GetSize())), maskPattern, aOptions, Some(sourceColor)); } void DrawTargetWebgl::MaskSurface(const Pattern& aSource, SourceSurface* aMask, Point aOffset, const DrawOptions& aOptions) { if (!SupportsDrawOptions(aOptions) || aSource.GetType() != PatternType::COLOR) { MarkSkiaChanged(aOptions); mSkia->MaskSurface(aSource, aMask, aOffset, aOptions); } else { auto sourceColor = static_cast(aSource).mColor; SurfacePattern pattern(aMask, ExtendMode::CLAMP, Matrix::Translation(aOffset)); DrawRect(Rect(aOffset, Size(aMask->GetSize())), pattern, aOptions, Some(sourceColor)); } } // Extract the surface's alpha values into an A8 surface. static already_AddRefed ExtractAlpha(SourceSurface* aSurface, bool aAllowSubpixelAA) { RefPtr surfaceData = aSurface->GetDataSurface(); if (!surfaceData) { return nullptr; } DataSourceSurface::ScopedMap srcMap(surfaceData, DataSourceSurface::READ); if (!srcMap.IsMapped()) { return nullptr; } IntSize size = surfaceData->GetSize(); RefPtr alpha = Factory::CreateDataSourceSurface(size, SurfaceFormat::A8, false); if (!alpha) { return nullptr; } DataSourceSurface::ScopedMap dstMap(alpha, DataSourceSurface::WRITE); if (!dstMap.IsMapped()) { return nullptr; } // For subpixel masks, ignore the alpha and instead sample one of the color // channels as if they were alpha. SwizzleData( srcMap.GetData(), srcMap.GetStride(), aAllowSubpixelAA ? SurfaceFormat::A8R8G8B8 : surfaceData->GetFormat(), dstMap.GetData(), dstMap.GetStride(), SurfaceFormat::A8, size); return alpha.forget(); } void DrawTargetWebgl::DrawShadow(const Path* aPath, const Pattern& aPattern, const ShadowOptions& aShadow, const DrawOptions& aOptions, const StrokeOptions* aStrokeOptions) { if (!aPath || aPath->GetBackendType() != BackendType::SKIA) { return; } // If there is a WebGL context, then try to cache the path to avoid slow // fallbacks. if (ShouldAccelPath(aOptions, aStrokeOptions) && mSharedContext->DrawPathAccel(aPath, aPattern, aOptions, aStrokeOptions, &aShadow)) { return; } // There was no path cache entry available to use, so fall back to drawing the // path with Skia. MarkSkiaChanged(aOptions); mSkia->DrawShadow(aPath, aPattern, aShadow, aOptions, aStrokeOptions); } void DrawTargetWebgl::DrawSurfaceWithShadow(SourceSurface* aSurface, const Point& aDest, const ShadowOptions& aShadow, CompositionOp aOperator) { DrawOptions options(1.0f, aOperator); if (ShouldAccelPath(options, nullptr)) { SurfacePattern pattern(aSurface, ExtendMode::CLAMP, Matrix::Translation(aDest)); SkPath skiaPath; skiaPath.addRect(RectToSkRect(Rect(aSurface->GetRect()) + aDest)); RefPtr path = new PathSkia(skiaPath, FillRule::FILL_WINDING); AutoRestoreTransform restore(this); SetTransform(Matrix()); if (mSharedContext->DrawPathAccel(path, pattern, options, nullptr, &aShadow, false)) { DrawRect(Rect(aSurface->GetRect()) + aDest, pattern, options); return; } } MarkSkiaChanged(options); mSkia->DrawSurfaceWithShadow(aSurface, aDest, aShadow, aOperator); } already_AddRefed DrawTargetWebgl::CreatePathBuilder( FillRule aFillRule) const { return mSkia->CreatePathBuilder(aFillRule); } void DrawTargetWebgl::SetTransform(const Matrix& aTransform) { DrawTarget::SetTransform(aTransform); mSkia->SetTransform(aTransform); } void DrawTargetWebgl::StrokeRect(const Rect& aRect, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) { if (!mWebglValid) { MarkSkiaChanged(aOptions); mSkia->StrokeRect(aRect, aPattern, aStrokeOptions, aOptions); } else { // If the stroke options are unsupported, then transform the rect to a path // so it can be cached. SkPath skiaPath; skiaPath.addRect(RectToSkRect(aRect)); RefPtr path = new PathSkia(skiaPath, FillRule::FILL_WINDING); DrawPath(path, aPattern, aOptions, &aStrokeOptions); } } static inline bool IsThinLine(const Matrix& aTransform, const StrokeOptions& aStrokeOptions) { auto scale = aTransform.ScaleFactors(); return std::max(scale.xScale, scale.yScale) * aStrokeOptions.mLineWidth <= 1; } bool DrawTargetWebgl::StrokeLineAccel(const Point& aStart, const Point& aEnd, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions, bool aClosed) { // Approximating a wide line as a rectangle works only with certain cap styles // in the general case (butt or square). However, if the line width is // sufficiently thin, we can either ignore the round cap (or treat it like // square for zero-length lines) without causing objectionable artifacts. // Lines may sometimes be used in closed paths that immediately reverse back, // in which case we need to use mLineJoin instead of mLineCap to determine the // actual cap used. CapStyle capStyle = aClosed ? (aStrokeOptions.mLineJoin == JoinStyle::ROUND ? CapStyle::ROUND : CapStyle::BUTT) : aStrokeOptions.mLineCap; if (mWebglValid && SupportsPattern(aPattern) && (capStyle != CapStyle::ROUND || IsThinLine(GetTransform(), aStrokeOptions)) && aStrokeOptions.mDashPattern == nullptr && aStrokeOptions.mLineWidth > 0) { // Treat the line as a rectangle whose center-line is the supplied line and // for which the height is the supplied line width. Generate a matrix that // maps the X axis to the orientation of the line and the Y axis to the // normal vector to the line. This only works if the line caps are squared, // as rounded rectangles are currently not supported for round line caps. Point start = aStart; Point dirX = aEnd - aStart; Point dirY; float dirLen = dirX.Length(); float scale = aStrokeOptions.mLineWidth; if (dirLen == 0.0f) { // If the line is zero-length, then only a cap is rendered. switch (capStyle) { case CapStyle::BUTT: // The cap doesn't extend beyond the line so nothing is drawn. return true; case CapStyle::ROUND: case CapStyle::SQUARE: // Draw a unit square centered at the single point. dirX = Point(scale, 0.0f); dirY = Point(0.0f, scale); // Offset the start by half a unit. start.x -= 0.5f * scale; break; } } else { // Make the scale map to a single unit length. scale /= dirLen; dirY = Point(-dirX.y, dirX.x) * scale; if (capStyle == CapStyle::SQUARE) { // Offset the start by half a unit. start -= (dirX * scale) * 0.5f; // Ensure the extent also accounts for the start and end cap. dirX += dirX * scale; } } Matrix lineXform(dirX.x, dirX.y, dirY.x, dirY.y, start.x - 0.5f * dirY.x, start.y - 0.5f * dirY.y); AutoRestoreTransform restore(this); ConcatTransform(lineXform); if (DrawRect(Rect(0, 0, 1, 1), aPattern, aOptions, Nothing(), nullptr, true, true, true)) { return true; } } return false; } void DrawTargetWebgl::StrokeLine(const Point& aStart, const Point& aEnd, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) { if (!mWebglValid) { MarkSkiaChanged(aOptions); mSkia->StrokeLine(aStart, aEnd, aPattern, aStrokeOptions, aOptions); } else if (!StrokeLineAccel(aStart, aEnd, aPattern, aStrokeOptions, aOptions)) { // If the stroke options are unsupported, then transform the line to a path // so it can be cached. SkPath skiaPath; skiaPath.moveTo(PointToSkPoint(aStart)); skiaPath.lineTo(PointToSkPoint(aEnd)); RefPtr path = new PathSkia(skiaPath, FillRule::FILL_WINDING); DrawPath(path, aPattern, aOptions, &aStrokeOptions); } } void DrawTargetWebgl::Stroke(const Path* aPath, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) { if (!aPath || aPath->GetBackendType() != BackendType::SKIA) { return; } const auto& skiaPath = static_cast(aPath)->GetPath(); if (!mWebglValid) { MarkSkiaChanged(aOptions); mSkia->Stroke(aPath, aPattern, aStrokeOptions, aOptions); return; } // Avoid using Skia's isLine here because some paths erroneously include a // closePath at the end, causing isLine to not detect the line. In that case // we just draw a line in reverse right over the original line. int numVerbs = skiaPath.countVerbs(); if (numVerbs >= 2 && numVerbs <= 3) { uint8_t verbs[3]; skiaPath.getVerbs(verbs, numVerbs); if (verbs[0] == SkPath::kMove_Verb && verbs[1] == SkPath::kLine_Verb && (numVerbs < 3 || verbs[2] == SkPath::kClose_Verb)) { bool closed = numVerbs >= 3; Point start = SkPointToPoint(skiaPath.getPoint(0)); Point end = SkPointToPoint(skiaPath.getPoint(1)); if (StrokeLineAccel(start, end, aPattern, aStrokeOptions, aOptions, closed)) { if (closed) { StrokeLineAccel(end, start, aPattern, aStrokeOptions, aOptions, true); } return; } // If accelerated line drawing failed, just treat it as a path. } } DrawPath(aPath, aPattern, aOptions, &aStrokeOptions); } bool DrawTargetWebgl::ShouldUseSubpixelAA(ScaledFont* aFont, const DrawOptions& aOptions) { AntialiasMode aaMode = aFont->GetDefaultAAMode(); if (aOptions.mAntialiasMode != AntialiasMode::DEFAULT) { aaMode = aOptions.mAntialiasMode; } return GetPermitSubpixelAA() && (aaMode == AntialiasMode::DEFAULT || aaMode == AntialiasMode::SUBPIXEL) && aOptions.mCompositionOp == CompositionOp::OP_OVER; } void DrawTargetWebgl::StrokeGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern, const StrokeOptions& aStrokeOptions, const DrawOptions& aOptions) { if (!aFont || !aBuffer.mNumGlyphs) { return; } bool useSubpixelAA = ShouldUseSubpixelAA(aFont, aOptions); if (mWebglValid && SupportsDrawOptions(aOptions) && aPattern.GetType() == PatternType::COLOR && PrepareContext() && mSharedContext->DrawGlyphsAccel(aFont, aBuffer, aPattern, aOptions, &aStrokeOptions, useSubpixelAA)) { return; } if (useSubpixelAA) { // Subpixel AA does not support layering because the subpixel masks can't // blend with the over op. MarkSkiaChanged(); } else { MarkSkiaChanged(aOptions); } mSkia->StrokeGlyphs(aFont, aBuffer, aPattern, aStrokeOptions, aOptions); } // Depending on whether we enable subpixel position for a given font, Skia may // round transformed coordinates differently on each axis. By default, text is // subpixel quantized horizontally and snapped to a whole integer vertical // baseline. Axis-flip transforms instead snap to horizontal boundaries while // subpixel quantizing along the vertical. For other types of transforms, Skia // just applies subpixel quantization to both axes. // We must duplicate the amount of quantization Skia applies carefully as a // boundary value such as 0.49 may round to 0.5 with subpixel quantization, // but if Skia actually snapped it to a whole integer instead, it would round // down to 0. If a subsequent glyph with offset 0.51 came in, we might // mistakenly round it down to 0.5, whereas Skia would round it up to 1. Thus // we would alias 0.49 and 0.51 to the same cache entry, while Skia would // actually snap the offset to 0 or 1, depending, resulting in mismatched // hinting. static inline IntPoint QuantizeScale(ScaledFont* aFont, const Matrix& aTransform) { if (!aFont->UseSubpixelPosition()) { return {1, 1}; } if (aTransform._12 == 0) { // Glyphs are rendered subpixel horizontally, so snap vertically. return {4, 1}; } if (aTransform._11 == 0) { // Glyphs are rendered subpixel vertically, so snap horizontally. return {1, 4}; } // The transform isn't aligned, so don't snap. return {4, 4}; } // Skia only supports subpixel positioning to the nearest 1/4 fraction. It // would be wasteful to attempt to cache text runs with positioning that is // anymore precise than this. To prevent this cache bloat, we quantize the // transformed glyph positions to the nearest 1/4. The scaling factor for // the quantization is baked into the transform, so that if subpixel rounding // is used on a given axis, then the axis will be multiplied by 4 before // rounding. Since the quantized position is not used for rasterization, the // transform is safe to modify as such. static inline IntPoint QuantizePosition(const Matrix& aTransform, const IntPoint& aOffset, const Point& aPosition) { return RoundedToInt(aTransform.TransformPoint(aPosition)) - aOffset; } // Get a quantized starting offset for the glyph buffer. We want this offset // to encapsulate the transform and buffer offset while still preserving the // relative subpixel positions of the glyphs this offset is subtracted from. static inline IntPoint QuantizeOffset(const Matrix& aTransform, const IntPoint& aQuantizeScale, const GlyphBuffer& aBuffer) { IntPoint offset = RoundedToInt(aTransform.TransformPoint(aBuffer.mGlyphs[0].mPosition)); offset.x.value &= ~(aQuantizeScale.x.value - 1); offset.y.value &= ~(aQuantizeScale.y.value - 1); return offset; } // Hashes a glyph buffer to a single hash value that can be used for quick // comparisons. Each glyph position is transformed and quantized before // hashing. HashNumber GlyphCacheEntry::HashGlyphs(const GlyphBuffer& aBuffer, const Matrix& aTransform, const IntPoint& aQuantizeScale) { HashNumber hash = 0; IntPoint offset = QuantizeOffset(aTransform, aQuantizeScale, aBuffer); for (size_t i = 0; i < aBuffer.mNumGlyphs; i++) { const Glyph& glyph = aBuffer.mGlyphs[i]; hash = AddToHash(hash, glyph.mIndex); IntPoint pos = QuantizePosition(aTransform, offset, glyph.mPosition); hash = AddToHash(hash, pos.x); hash = AddToHash(hash, pos.y); } return hash; } // Determines if an existing glyph cache entry matches an incoming text run. inline bool GlyphCacheEntry::MatchesGlyphs( const GlyphBuffer& aBuffer, const DeviceColor& aColor, const Matrix& aTransform, const IntPoint& aQuantizeOffset, const IntPoint& aBoundsOffset, const IntRect& aClipRect, HashNumber aHash, const StrokeOptions* aStrokeOptions) { // First check if the hash matches to quickly reject the text run before any // more expensive checking. If it matches, then check if the color and // transform are the same. if (aHash != mHash || aBuffer.mNumGlyphs != mBuffer.mNumGlyphs || aColor != mColor || !HasMatchingScale(aTransform, mTransform)) { return false; } // Finally check if all glyphs and their quantized positions match. for (size_t i = 0; i < aBuffer.mNumGlyphs; i++) { const Glyph& dst = mBuffer.mGlyphs[i]; const Glyph& src = aBuffer.mGlyphs[i]; if (dst.mIndex != src.mIndex || dst.mPosition != Point(QuantizePosition(aTransform, aQuantizeOffset, src.mPosition))) { return false; } } // Check that stroke options actually match. if (aStrokeOptions) { // If stroking, verify that the entry is also stroked with the same options. if (!(mStrokeOptions && *aStrokeOptions == *mStrokeOptions)) { return false; } } else if (mStrokeOptions) { // If not stroking, check if the entry is stroked. If so, don't match. return false; } // Verify that the full bounds, once translated and clipped, are equal to the // clipped bounds. return (mFullBounds + aBoundsOffset) .Intersect(aClipRect) .IsEqualEdges(GetBounds() + aBoundsOffset); } GlyphCacheEntry::GlyphCacheEntry(const GlyphBuffer& aBuffer, const DeviceColor& aColor, const Matrix& aTransform, const IntPoint& aQuantizeScale, const IntRect& aBounds, const IntRect& aFullBounds, HashNumber aHash, StoredStrokeOptions* aStrokeOptions) : CacheEntryImpl(aTransform, aBounds, aHash), mColor(aColor), mFullBounds(aFullBounds), mStrokeOptions(aStrokeOptions) { // Store a copy of the glyph buffer with positions already quantized for fast // comparison later. Glyph* glyphs = new Glyph[aBuffer.mNumGlyphs]; IntPoint offset = QuantizeOffset(aTransform, aQuantizeScale, aBuffer); // Make the bounds relative to the offset so we can add a new offset later. IntPoint boundsOffset(offset.x / aQuantizeScale.x, offset.y / aQuantizeScale.y); mBounds -= boundsOffset; mFullBounds -= boundsOffset; for (size_t i = 0; i < aBuffer.mNumGlyphs; i++) { Glyph& dst = glyphs[i]; const Glyph& src = aBuffer.mGlyphs[i]; dst.mIndex = src.mIndex; dst.mPosition = Point(QuantizePosition(aTransform, offset, src.mPosition)); } mBuffer.mGlyphs = glyphs; mBuffer.mNumGlyphs = aBuffer.mNumGlyphs; } GlyphCacheEntry::~GlyphCacheEntry() { delete[] mBuffer.mGlyphs; } // Attempt to find a matching entry in the glyph cache. The caller should check // whether the contained texture handle is valid to determine if it will need to // render the text run or just reuse the cached texture. already_AddRefed GlyphCache::FindEntry( const GlyphBuffer& aBuffer, const DeviceColor& aColor, const Matrix& aTransform, const IntPoint& aQuantizeScale, const IntRect& aClipRect, HashNumber aHash, const StrokeOptions* aStrokeOptions) { IntPoint offset = QuantizeOffset(aTransform, aQuantizeScale, aBuffer); IntPoint boundsOffset(offset.x / aQuantizeScale.x, offset.y / aQuantizeScale.y); for (const RefPtr& entry : GetChain(aHash)) { if (entry->MatchesGlyphs(aBuffer, aColor, aTransform, offset, boundsOffset, aClipRect, aHash, aStrokeOptions)) { return do_AddRef(entry); } } return nullptr; } // Insert a new entry in the glyph cache. already_AddRefed GlyphCache::InsertEntry( const GlyphBuffer& aBuffer, const DeviceColor& aColor, const Matrix& aTransform, const IntPoint& aQuantizeScale, const IntRect& aBounds, const IntRect& aFullBounds, HashNumber aHash, const StrokeOptions* aStrokeOptions) { StoredStrokeOptions* strokeOptions = nullptr; if (aStrokeOptions) { strokeOptions = aStrokeOptions->Clone(); if (!strokeOptions) { return nullptr; } } RefPtr entry = new GlyphCacheEntry(aBuffer, aColor, aTransform, aQuantizeScale, aBounds, aFullBounds, aHash, strokeOptions); Insert(entry); return entry.forget(); } GlyphCache::GlyphCache(ScaledFont* aFont) : mFont(aFont) {} static void ReleaseGlyphCache(void* aPtr) { delete static_cast(aPtr); } void DrawTargetWebgl::SetPermitSubpixelAA(bool aPermitSubpixelAA) { DrawTarget::SetPermitSubpixelAA(aPermitSubpixelAA); mSkia->SetPermitSubpixelAA(aPermitSubpixelAA); } // Check for any color glyphs contained within a rasterized BGRA8 text result. static bool CheckForColorGlyphs(const RefPtr& aSurface) { if (aSurface->GetFormat() != SurfaceFormat::B8G8R8A8) { return false; } RefPtr dataSurf = aSurface->GetDataSurface(); if (!dataSurf) { return true; } DataSourceSurface::ScopedMap map(dataSurf, DataSourceSurface::READ); if (!map.IsMapped()) { return true; } IntSize size = dataSurf->GetSize(); const uint8_t* data = map.GetData(); int32_t stride = map.GetStride(); for (int y = 0; y < size.height; y++) { const uint32_t* x = (const uint32_t*)data; const uint32_t* end = x + size.width; for (; x < end; x++) { // Verify if all components are the same as for premultiplied grayscale. uint32_t color = *x; uint32_t gray = color & 0xFF; gray |= gray << 8; gray |= gray << 16; if (color != gray) return true; } data += stride; } return false; } // Draws glyphs to the WebGL target by trying to generate a cached texture for // the text run that can be subsequently reused to quickly render the text run // without using any software surfaces. bool DrawTargetWebgl::SharedContext::DrawGlyphsAccel( ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern, const DrawOptions& aOptions, const StrokeOptions* aStrokeOptions, bool aUseSubpixelAA) { // Whether the font may use bitmaps. If so, we need to render the glyphs with // color as grayscale bitmaps will use the color while color emoji will not, // with no easy way to know ahead of time. We currently have to check the // rasterized result to see if there are any color glyphs. To render subpixel // masks, we need to know that the rasterized result actually represents a // subpixel mask rather than try to interpret it as a normal RGBA result such // as for color emoji. bool useBitmaps = !aStrokeOptions && aFont->MayUseBitmaps() && aOptions.mCompositionOp != CompositionOp::OP_CLEAR; // Look for an existing glyph cache on the font. If not there, create it. GlyphCache* cache = static_cast(aFont->GetUserData(&mGlyphCacheKey)); if (!cache) { cache = new GlyphCache(aFont); aFont->AddUserData(&mGlyphCacheKey, cache, ReleaseGlyphCache); mGlyphCaches.insertFront(cache); } // Hash the incoming text run and looking for a matching entry. DeviceColor color = aOptions.mCompositionOp == CompositionOp::OP_CLEAR ? DeviceColor(1, 1, 1, 1) : static_cast(aPattern).mColor; #ifdef XP_MACOSX // On macOS, depending on whether the text is classified as light-on-dark or // dark-on-light, we may end up with different amounts of dilation applied, so // we can't use the same mask in the two circumstances, or the glyphs will be // dilated incorrectly. bool lightOnDark = useBitmaps || (color.r >= 0.33f && color.g >= 0.33f && color.b >= 0.33f && color.r + color.g + color.b >= 2.0f); #else // On other platforms, we assume no color-dependent dilation. const bool lightOnDark = true; #endif // If the font has bitmaps, use the color directly. Otherwise, the texture // will hold a grayscale mask, so encode the key's subpixel and light-or-dark // state in the color. const Matrix& currentTransform = GetTransform(); IntPoint quantizeScale = QuantizeScale(aFont, currentTransform); Matrix quantizeTransform = currentTransform; quantizeTransform.PostScale(quantizeScale.x, quantizeScale.y); HashNumber hash = GlyphCacheEntry::HashGlyphs(aBuffer, quantizeTransform, quantizeScale); DeviceColor colorOrMask = useBitmaps ? color : DeviceColor::Mask(aUseSubpixelAA ? 1 : 0, lightOnDark ? 1 : 0); IntRect clipRect(IntPoint(), mViewportSize); RefPtr entry = cache->FindEntry(aBuffer, colorOrMask, quantizeTransform, quantizeScale, clipRect, hash, aStrokeOptions); if (!entry) { // For small text runs, bounds computations can be expensive relative to the // cost of looking up a cache result. Avoid doing local bounds computations // until actually inserting the entry into the cache. Maybe bounds = mCurrentTarget->mSkia->GetGlyphLocalBounds( aFont, aBuffer, aPattern, aStrokeOptions, aOptions); if (!bounds) { return true; } // Transform the local bounds into device space so that we know how big // the cached texture will be. Rect xformBounds = currentTransform.TransformBounds(*bounds); // Check if the transform flattens out the bounds before rounding. if (xformBounds.IsEmpty()) { return true; } IntRect fullBounds = RoundedOut(currentTransform.TransformBounds(*bounds)); IntRect clipBounds = fullBounds.Intersect(clipRect); // Check if the bounds are completely clipped out. if (clipBounds.IsEmpty()) { return true; } entry = cache->InsertEntry(aBuffer, colorOrMask, quantizeTransform, quantizeScale, clipBounds, fullBounds, hash, aStrokeOptions); if (!entry) { return false; } } // The bounds of the entry may have a different transform offset from the // bounds of the currently drawn text run. The entry bounds are relative to // the entry's quantized offset already, so just move the bounds to the new // offset. IntRect intBounds = entry->GetBounds(); IntPoint newOffset = QuantizeOffset(quantizeTransform, quantizeScale, aBuffer); intBounds += IntPoint(newOffset.x / quantizeScale.x, newOffset.y / quantizeScale.y); // Ensure there is a clear border around the text. This must be applied only // after clipping so that we always have some border texels for filtering. intBounds.Inflate(2); RefPtr handle = entry->GetHandle(); if (handle && handle->IsValid()) { // If there is an entry with a valid cached texture handle, then try // to draw with that. If that for some reason failed, then fall back // to using the Skia target as that means we were preventing from // drawing to the WebGL context based on something other than the // texture. SurfacePattern pattern(nullptr, ExtendMode::CLAMP, Matrix::Translation(intBounds.TopLeft())); if (DrawRectAccel(Rect(intBounds), pattern, aOptions, useBitmaps ? Nothing() : Some(color), &handle, false, true, true)) { return true; } } else { handle = nullptr; // If we get here, either there wasn't a cached texture handle or it // wasn't valid. Render the text run into a temporary target. RefPtr textDT = new DrawTargetSkia; if (textDT->Init(intBounds.Size(), lightOnDark && !useBitmaps && !aUseSubpixelAA ? SurfaceFormat::A8 : SurfaceFormat::B8G8R8A8)) { if (!lightOnDark) { // If rendering dark-on-light text, we need to clear the background to // white while using an opaque alpha value to allow this. textDT->FillRect(Rect(IntRect(IntPoint(), intBounds.Size())), ColorPattern(DeviceColor(1, 1, 1, 1)), DrawOptions(1.0f, CompositionOp::OP_OVER)); } textDT->SetTransform(currentTransform * Matrix::Translation(-intBounds.TopLeft())); textDT->SetPermitSubpixelAA(aUseSubpixelAA); DrawOptions drawOptions(1.0f, CompositionOp::OP_OVER, aOptions.mAntialiasMode); // If bitmaps might be used, then we have to supply the color, as color // emoji may ignore it while grayscale bitmaps may use it, with no way to // know ahead of time. Otherwise, assume the output will be a mask and // just render it white to determine intensity. Depending on whether the // text is light or dark, we render white or black text respectively. ColorPattern colorPattern( useBitmaps ? color : DeviceColor::Mask(lightOnDark ? 1 : 0, 1)); if (aStrokeOptions) { textDT->StrokeGlyphs(aFont, aBuffer, colorPattern, *aStrokeOptions, drawOptions); } else { textDT->FillGlyphs(aFont, aBuffer, colorPattern, drawOptions); } if (!lightOnDark) { uint8_t* data = nullptr; IntSize size; int32_t stride = 0; SurfaceFormat format = SurfaceFormat::UNKNOWN; if (!textDT->LockBits(&data, &size, &stride, &format)) { return false; } uint8_t* row = data; for (int y = 0; y < size.height; ++y) { uint8_t* px = row; for (int x = 0; x < size.width; ++x) { // If rendering dark-on-light text, we need to invert the final mask // so that it is in the expected white text on transparent black // format. The alpha will be initialized to the largest of the // values. px[0] = 255 - px[0]; px[1] = 255 - px[1]; px[2] = 255 - px[2]; px[3] = std::max(px[0], std::max(px[1], px[2])); px += 4; } row += stride; } textDT->ReleaseBits(data); } RefPtr textSurface = textDT->Snapshot(); if (textSurface) { // If we don't expect the text surface to contain color glyphs // such as from subpixel AA, then do one final check to see if // any ended up in the result. If not, extract the alpha values // from the surface so we can render it as a mask. if (textSurface->GetFormat() != SurfaceFormat::A8 && !CheckForColorGlyphs(textSurface)) { textSurface = ExtractAlpha(textSurface, !useBitmaps); if (!textSurface) { // Failed extracting alpha for the text surface... return false; } } // Attempt to upload the rendered text surface into a texture // handle and draw it. SurfacePattern pattern(textSurface, ExtendMode::CLAMP, Matrix::Translation(intBounds.TopLeft())); if (DrawRectAccel(Rect(intBounds), pattern, aOptions, useBitmaps ? Nothing() : Some(color), &handle, false, true) && handle) { // If drawing succeeded, then the text surface was uploaded to // a texture handle. Assign it to the glyph cache entry. entry->Link(handle); } else { // If drawing failed, remove the entry from the cache. entry->Unlink(); } return true; } } } return false; } void DrawTargetWebgl::FillGlyphs(ScaledFont* aFont, const GlyphBuffer& aBuffer, const Pattern& aPattern, const DrawOptions& aOptions) { if (!aFont || !aBuffer.mNumGlyphs) { return; } bool useSubpixelAA = ShouldUseSubpixelAA(aFont, aOptions); if (mWebglValid && SupportsDrawOptions(aOptions) && aPattern.GetType() == PatternType::COLOR && PrepareContext() && mSharedContext->DrawGlyphsAccel(aFont, aBuffer, aPattern, aOptions, nullptr, useSubpixelAA)) { return; } // If not able to cache the text run to a texture, then just fall back to // drawing with the Skia target. if (useSubpixelAA) { // Subpixel AA does not support layering because the subpixel masks can't // blend with the over op. MarkSkiaChanged(); } else { MarkSkiaChanged(aOptions); } mSkia->FillGlyphs(aFont, aBuffer, aPattern, aOptions); } void DrawTargetWebgl::SharedContext::WaitForShmem(DrawTargetWebgl* aTarget) { if (mWaitForShmem) { // GetError is a sync IPDL call that forces all dispatched commands to be // flushed. Once it returns, we are certain that any commands processing // the Shmem have finished. (void)mWebgl->GetError(); mWaitForShmem = false; // The sync IPDL call can cause expensive round-trips to add up over time, // so account for that here. if (aTarget) { aTarget->mProfile.OnReadback(); } } } void DrawTargetWebgl::MarkSkiaChanged(const DrawOptions& aOptions) { if (SupportsLayering(aOptions)) { WaitForShmem(); if (!mSkiaValid) { // If the Skia context needs initialization, clear it and enable layering. mSkiaValid = true; if (mWebglValid) { mProfile.OnLayer(); mSkiaLayer = true; mSkiaLayerClear = mIsClear; mSkia->DetachAllSnapshots(); if (mSkiaLayerClear) { // Avoid blending later by making sure the layer background is filled // with opaque alpha values if necessary. mSkiaNoClip->FillRect(Rect(mSkiaNoClip->GetRect()), GetClearPattern(), DrawOptions(1.0f, CompositionOp::OP_SOURCE)); } else { mSkiaNoClip->ClearRect(Rect(mSkiaNoClip->GetRect())); } } } // The WebGL context is no longer up-to-date. mWebglValid = false; mIsClear = false; } else { // For other composition ops, just overwrite the Skia data. MarkSkiaChanged(); } } // Attempts to read the contents of the WebGL context into the Skia target. void DrawTargetWebgl::ReadIntoSkia() { if (mSkiaValid) { return; } if (mWebglValid) { uint8_t* data = nullptr; IntSize size; int32_t stride; SurfaceFormat format; if (mIsClear) { // If the WebGL target is still clear, then just clear the Skia target. mSkia->DetachAllSnapshots(); mSkiaNoClip->FillRect(Rect(mSkiaNoClip->GetRect()), GetClearPattern(), DrawOptions(1.0f, CompositionOp::OP_SOURCE)); } else { // If there's no existing snapshot and we can successfully map the Skia // target for reading, then try to read into that. if (!mSnapshot && mSkia->LockBits(&data, &size, &stride, &format)) { (void)ReadInto(data, stride); mSkia->ReleaseBits(data); } else if (RefPtr snapshot = Snapshot()) { // Otherwise, fall back to getting a snapshot from WebGL if available // and then copying that to Skia. mSkia->CopySurface(snapshot, GetRect(), IntPoint(0, 0)); } // Signal that we've hit a complete software fallback. mProfile.OnFallback(); } } mSkiaValid = true; // The Skia data is flat after reading, so disable any layering. mSkiaLayer = false; } // Reads data from the WebGL context and blends it with the current Skia layer. void DrawTargetWebgl::FlattenSkia() { if (!mSkiaValid || !mSkiaLayer) { return; } mSkiaLayer = false; if (mSkiaLayerClear) { // If the WebGL target is clear, then there is nothing to blend. return; } if (RefPtr base = ReadSnapshot()) { mSkia->DetachAllSnapshots(); mSkiaNoClip->DrawSurface(base, Rect(GetRect()), Rect(GetRect()), DrawSurfaceOptions(SamplingFilter::POINT), DrawOptions(1.f, CompositionOp::OP_DEST_OVER)); } } // Attempts to draw the contents of the Skia target into the WebGL context. bool DrawTargetWebgl::FlushFromSkia() { // If the WebGL context has been lost, then mark it as invalid and fail. if (mSharedContext->IsContextLost()) { mWebglValid = false; return false; } // The WebGL target is already valid, so there is nothing to do. if (mWebglValid) { return true; } // Ensure that DrawRect doesn't recursively call into FlushFromSkia. If // the Skia target isn't valid, then it doesn't matter what is in the the // WebGL target either, so only try to blend if there is a valid Skia target. mWebglValid = true; if (mSkiaValid) { AutoRestoreContext restore(this); // If the Skia target is clear, then there is no need to use a snapshot. // Directly clear the WebGL target instead. if (mIsClear) { if (!DrawRect(Rect(GetRect()), GetClearPattern(), DrawOptions(1.0f, CompositionOp::OP_SOURCE), Nothing(), nullptr, false, false, true)) { mWebglValid = false; return false; } return true; } RefPtr skiaSnapshot = mSkia->Snapshot(); if (!skiaSnapshot) { // There's a valid Skia target to draw to, but for some reason there is // no available snapshot, so just keep using the Skia target. mWebglValid = false; return false; } // If there is no layer, then just upload it directly. if (!mSkiaLayer) { if (PrepareContext(false) && MarkChanged()) { if (RefPtr data = skiaSnapshot->GetDataSurface()) { mSharedContext->UploadSurface(data, mFormat, GetRect(), IntPoint(), false, false, mTex); return true; } } // Failed to upload the Skia snapshot. mWebglValid = false; return false; } SurfacePattern pattern(skiaSnapshot, ExtendMode::CLAMP); // If there is a layer, blend the snapshot with the WebGL context. if (!DrawRect(Rect(GetRect()), pattern, DrawOptions(1.0f, CompositionOp::OP_OVER), Nothing(), &mSnapshotTexture, false, false, true, true)) { // If accelerated drawing failed for some reason, then leave the Skia // target unchanged. mWebglValid = false; return false; } } return true; } void DrawTargetWebgl::UsageProfile::BeginFrame() { // Reset the usage profile counters for the new frame. mFallbacks = 0; mLayers = 0; mCacheMisses = 0; mCacheHits = 0; mUncachedDraws = 0; mReadbacks = 0; } void DrawTargetWebgl::UsageProfile::EndFrame() { bool failed = false; // If we hit a complete fallback to software rendering, or if cache misses // were more than cutoff ratio of all requests, then we consider the frame as // having failed performance profiling. float cacheRatio = StaticPrefs::gfx_canvas_accelerated_profile_cache_miss_ratio(); if (mFallbacks > 0 || float(mCacheMisses + mReadbacks + mLayers) > cacheRatio * float(mCacheMisses + mCacheHits + mUncachedDraws + mReadbacks + mLayers)) { failed = true; } if (failed) { ++mFailedFrames; } ++mFrameCount; } bool DrawTargetWebgl::UsageProfile::RequiresRefresh() const { // If we've rendered at least the required number of frames for a profile and // more than the cutoff ratio of frames did not meet performance criteria, // then we should stop using an accelerated canvas. uint32_t profileFrames = StaticPrefs::gfx_canvas_accelerated_profile_frames(); if (!profileFrames || mFrameCount < profileFrames) { return false; } float failRatio = StaticPrefs::gfx_canvas_accelerated_profile_fallback_ratio(); return mFailedFrames > failRatio * mFrameCount; } void DrawTargetWebgl::SharedContext::CachePrefs() { uint32_t capacity = StaticPrefs::gfx_canvas_accelerated_gpu_path_size() << 20; if (capacity != mPathVertexCapacity) { mPathVertexCapacity = capacity; if (mPathCache) { mPathCache->ClearVertexRanges(); } if (mPathVertexBuffer) { ResetPathVertexBuffer(); } } mPathMaxComplexity = StaticPrefs::gfx_canvas_accelerated_gpu_path_complexity(); mPathAAStroke = StaticPrefs::gfx_canvas_accelerated_aa_stroke_enabled(); mPathWGRStroke = StaticPrefs::gfx_canvas_accelerated_stroke_to_fill_path(); } // For use within CanvasRenderingContext2D, called on BorrowDrawTarget. void DrawTargetWebgl::BeginFrame(const IntRect& aPersistedRect) { if (mNeedsPresent) { mNeedsPresent = false; // If still rendering into the Skia target, switch back to the WebGL // context. if (!mWebglValid) { if (aPersistedRect.IsEmpty()) { // If nothing needs to persist, just mark the WebGL context valid. mWebglValid = true; // Even if the Skia framebuffer is marked clear, since the WebGL // context is not valid, its contents may be out-of-date and not // necessarily clear. mIsClear = false; } else { FlushFromSkia(); } } } // Check if we need to clear out any cached because of memory pressure. mSharedContext->ClearCachesIfNecessary(); // Cache any prefs for the frame. mSharedContext->CachePrefs(); mProfile.BeginFrame(); } // For use within CanvasRenderingContext2D, called on ReturnDrawTarget. void DrawTargetWebgl::EndFrame() { if (StaticPrefs::gfx_canvas_accelerated_debug()) { // Draw a green rectangle in the upper right corner to indicate // acceleration. IntRect corner = IntRect(mSize.width - 16, 0, 16, 16).Intersect(GetRect()); DrawRect(Rect(corner), ColorPattern(DeviceColor(0.0f, 1.0f, 0.0f, 1.0f)), DrawOptions(), Nothing(), nullptr, false, false); } mProfile.EndFrame(); // Ensure we're not somehow using more than the allowed texture memory. mSharedContext->PruneTextureMemory(); // Signal that we're done rendering the frame in case no present occurs. mSharedContext->mWebgl->EndOfFrame(); // Check if we need to clear out any cached because of memory pressure. mSharedContext->ClearCachesIfNecessary(); // The framebuffer is dirty, so it needs to be copied to the swapchain. mNeedsPresent = true; } Maybe DrawTargetWebgl::GetFrontBuffer() { // Only try to present and retrieve the front buffer if there is a valid // WebGL framebuffer that can be sent to the compositor. Otherwise, return // nothing to try to reuse the Skia snapshot. if (mNeedsPresent) { mNeedsPresent = false; if (mWebglValid || FlushFromSkia()) { // Copy and swizzle the WebGL framebuffer to the swap chain front buffer. webgl::SwapChainOptions options; options.bgra = true; // Allow async present to be toggled on for accelerated Canvas2D // independent of WebGL via pref. options.forceAsyncPresent = StaticPrefs::gfx_canvas_accelerated_async_present(); mSharedContext->mWebgl->CopyToSwapChain(mFramebuffer, options); } } if (mWebglValid) { return mSharedContext->mWebgl->GetFrontBuffer(mFramebuffer); } return Nothing(); } already_AddRefed DrawTargetWebgl::CreateSimilarDrawTarget( const IntSize& aSize, SurfaceFormat aFormat) const { return mSkia->CreateSimilarDrawTarget(aSize, aFormat); } bool DrawTargetWebgl::CanCreateSimilarDrawTarget(const IntSize& aSize, SurfaceFormat aFormat) const { return mSkia->CanCreateSimilarDrawTarget(aSize, aFormat); } RefPtr DrawTargetWebgl::CreateClippedDrawTarget( const Rect& aBounds, SurfaceFormat aFormat) { return mSkia->CreateClippedDrawTarget(aBounds, aFormat); } already_AddRefed DrawTargetWebgl::CreateSourceSurfaceFromData( unsigned char* aData, const IntSize& aSize, int32_t aStride, SurfaceFormat aFormat) const { return mSkia->CreateSourceSurfaceFromData(aData, aSize, aStride, aFormat); } already_AddRefed DrawTargetWebgl::CreateSourceSurfaceFromNativeSurface( const NativeSurface& aSurface) const { return mSkia->CreateSourceSurfaceFromNativeSurface(aSurface); } already_AddRefed DrawTargetWebgl::OptimizeSourceSurface( SourceSurface* aSurface) const { if (aSurface->GetType() == SurfaceType::WEBGL) { return do_AddRef(aSurface); } return mSkia->OptimizeSourceSurface(aSurface); } already_AddRefed DrawTargetWebgl::OptimizeSourceSurfaceForUnknownAlpha( SourceSurface* aSurface) const { return mSkia->OptimizeSourceSurfaceForUnknownAlpha(aSurface); } already_AddRefed DrawTargetWebgl::CreateGradientStops( GradientStop* aStops, uint32_t aNumStops, ExtendMode aExtendMode) const { return mSkia->CreateGradientStops(aStops, aNumStops, aExtendMode); } already_AddRefed DrawTargetWebgl::CreateFilter(FilterType aType) { return mSkia->CreateFilter(aType); } void DrawTargetWebgl::DrawFilter(FilterNode* aNode, const Rect& aSourceRect, const Point& aDestPoint, const DrawOptions& aOptions) { MarkSkiaChanged(aOptions); mSkia->DrawFilter(aNode, aSourceRect, aDestPoint, aOptions); } bool DrawTargetWebgl::Draw3DTransformedSurface(SourceSurface* aSurface, const Matrix4x4& aMatrix) { MarkSkiaChanged(); return mSkia->Draw3DTransformedSurface(aSurface, aMatrix); } void DrawTargetWebgl::PushLayer(bool aOpaque, Float aOpacity, SourceSurface* aMask, const Matrix& aMaskTransform, const IntRect& aBounds, bool aCopyBackground) { PushLayerWithBlend(aOpaque, aOpacity, aMask, aMaskTransform, aBounds, aCopyBackground, CompositionOp::OP_OVER); } void DrawTargetWebgl::PushLayerWithBlend(bool aOpaque, Float aOpacity, SourceSurface* aMask, const Matrix& aMaskTransform, const IntRect& aBounds, bool aCopyBackground, CompositionOp aCompositionOp) { MarkSkiaChanged(DrawOptions(aOpacity, aCompositionOp)); mSkia->PushLayerWithBlend(aOpaque, aOpacity, aMask, aMaskTransform, aBounds, aCopyBackground, aCompositionOp); ++mLayerDepth; } void DrawTargetWebgl::PopLayer() { MOZ_ASSERT(mSkiaValid); MOZ_ASSERT(mLayerDepth > 0); --mLayerDepth; mSkia->PopLayer(); } } // namespace mozilla::gfx