/* -*- 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/PathHelpers.h"
#include "mozilla/gfx/PathSkia.h"
#include "mozilla/gfx/Swizzle.h"
#include "mozilla/layers/ImageDataSerializer.h"
#include "mozilla/layers/RemoteTextureMap.h"
#include "mozilla/widget/ScreenManager.h"
#include "skia/include/core/SkPixmap.h"
#include "nsContentUtils.h"
#include "GLContext.h"
#include "WebGLContext.h"
#include "WebGLChild.h"
#include "WebGLBuffer.h"
#include "WebGLFramebuffer.h"
#include "WebGLProgram.h"
#include "WebGLShader.h"
#include "WebGLTexture.h"
#include "WebGLVertexArray.h"
#include "gfxPlatform.h"
#ifdef XP_MACOSX
# include
"mozilla/gfx/ScaledFontMac.h"
#endif
namespace mozilla::gfx {
BackingTexture::BackingTexture(
const IntSize& aSize, SurfaceFormat aFormat,
const RefPtr<WebGLTexture>& aTexture)
: mSize(aSize), mFormat(aFormat), mTexture(aTexture) {}
#ifdef XP_WIN
// Work around buggy ANGLE/D3D drivers that may copy blocks of pixels outside
// the row length. Extra space is reserved at the end of each row up to stride
// alignment. This does not affect standalone textures.
static const Etagere::AllocatorOptions kR8AllocatorOptions = {16, 1, 1, 0};
#endif
SharedTexture::SharedTexture(
const IntSize& aSize, SurfaceFormat aFormat,
const RefPtr<WebGLTexture>& aTexture)
: BackingTexture(aSize, aFormat, aTexture),
mAtlasAllocator(
#ifdef XP_WIN
aFormat == SurfaceFormat::A8
? Etagere::etagere_atlas_allocator_with_options(
aSize.width, aSize.height, &kR8AllocatorOptions)
:
#endif
Etagere::etagere_atlas_allocator_new(aSize.width, aSize.height)) {
}
SharedTexture::~SharedTexture() {
if (mAtlasAllocator) {
Etagere::etagere_atlas_allocator_delete(mAtlasAllocator);
mAtlasAllocator = nullptr;
}
}
SharedTextureHandle::SharedTextureHandle(Etagere::AllocationId aId,
const IntRect& aBounds,
SharedTexture* aTexture)
: mAllocationId(aId), mBounds(aBounds), mTexture(aTexture) {}
already_AddRefed<SharedTextureHandle> SharedTexture::Allocate(
const IntSize& aSize) {
Etagere::Allocation alloc = {{0, 0, 0, 0}, Etagere::INVALID_ALLOCATION_ID};
if (!mAtlasAllocator ||
!Etagere::etagere_atlas_allocator_allocate(mAtlasAllocator, aSize.width,
aSize.height, &alloc) ||
alloc.id == Etagere::INVALID_ALLOCATION_ID) {
return nullptr;
}
RefPtr<SharedTextureHandle> handle =
new SharedTextureHandle(
alloc.id,
IntRect(IntPoint(alloc.rectangle.min_x, alloc.rectangle.min_y), aSize),
this);
return handle.forget();
}
bool SharedTexture::Free(SharedTextureHandle& aHandle) {
if (aHandle.mTexture !=
this) {
return false;
}
if (aHandle.mAllocationId != Etagere::INVALID_ALLOCATION_ID) {
if (mAtlasAllocator) {
Etagere::etagere_atlas_allocator_deallocate(mAtlasAllocator,
aHandle.mAllocationId);
}
aHandle.mAllocationId = Etagere::INVALID_ALLOCATION_ID;
}
return true;
}
StandaloneTexture::StandaloneTexture(
const IntSize& aSize,
SurfaceFormat aFormat,
const RefPtr<WebGLTexture>& aTexture)
: BackingTexture(aSize, aFormat, aTexture) {}
DrawTargetWebgl::DrawTargetWebgl() =
default;
inline void SharedContextWebgl::ClearLastTexture(
bool aFullClear) {
mLastTexture = nullptr;
if (aFullClear) {
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();
RefPtr<SourceSurfaceWebgl> snapshot = mSnapshot.forget();
if (snapshot->hasOneRef()) {
return;
}
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) {
// Force any Skia snapshots to copy the shmem before it deallocs.
if (mSkia) {
mSkia->DetachAllSnapshots();
}
mSharedContext->ClearLastTexture(
true);
mClipMask = nullptr;
mFramebuffer = nullptr;
mTex = nullptr;
mSharedContext->mDrawTargetCount--;
}
}
SharedContextWebgl::SharedContextWebgl() =
default;
SharedContextWebgl::~SharedContextWebgl() {
// Detect context loss before deletion.
if (mWebgl) {
ExitTlsScope();
mWebgl->ActiveTexture(0);
}
if (mWGRPathBuilder) {
WGR::wgr_builder_release(mWGRPathBuilder);
mWGRPathBuilder = nullptr;
}
ClearAllTextures();
UnlinkSurfaceTextures();
UnlinkGlyphCaches();
}
gl::GLContext* SharedContextWebgl::GetGLContext() {
return mWebgl ? mWebgl->GL() : nullptr;
}
void SharedContextWebgl::EnterTlsScope() {
if (mTlsScope.isSome()) {
return;
}
if (gl::GLContext* gl = GetGLContext()) {
mTlsScope = Some(gl->mUseTLSIsCurrent);
gl::GLContext::InvalidateCurrentContext();
gl->mUseTLSIsCurrent =
true;
}
}
void SharedContextWebgl::ExitTlsScope() {
if (mTlsScope.isNothing()) {
return;
}
if (gl::GLContext* gl = GetGLContext()) {
gl->mUseTLSIsCurrent = mTlsScope.value();
}
mTlsScope = Nothing();
}
// Remove any SourceSurface user data associated with this TextureHandle.
inline void SharedContextWebgl::UnlinkSurfaceTexture(
const RefPtr<TextureHandle>& aHandle) {
if (RefPtr<SourceSurface> surface = aHandle->GetSurface()) {
// Ensure any WebGL snapshot textures get unlinked.
if (surface->GetType() == SurfaceType::WEBGL) {
static_cast<SourceSurfaceWebgl*>(surface.get())->OnUnlinkTexture(
this);
}
surface->RemoveUserData(aHandle->IsShadow() ? &mShadowTextureKey
: &mTextureHandleKey);
}
}
// Unlinks TextureHandles from any SourceSurface user data.
void SharedContextWebgl::UnlinkSurfaceTextures() {
for (RefPtr<TextureHandle> handle = mTextureHandles.getFirst(); handle;
handle = handle->getNext()) {
UnlinkSurfaceTexture(handle);
}
}
// Unlinks GlyphCaches from any ScaledFont user data.
void SharedContextWebgl::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 SharedContextWebgl::OnMemoryPressure() { mShouldClearCaches =
true; }
void SharedContextWebgl::ClearCaches() {
OnMemoryPressure();
ClearCachesIfNecessary();
}
// Clear out the entire list of texture handles from any source.
void SharedContextWebgl::ClearAllTextures() {
while (!mTextureHandles.isEmpty()) {
PruneTextureHandle(mTextureHandles.popLast());
--mNumTextureHandles;
}
}
// Scan through the shared texture pages looking for any that are empty and
// delete them.
void SharedContextWebgl::ClearEmptyTextureMemory() {
for (
auto pos = mSharedTextures.begin(); pos != mSharedTextures.end();) {
if (!(*pos)->HasAllocatedHandles()) {
RefPtr<SharedTexture> shared = *pos;
size_t usedBytes = shared->UsedBytes();
mEmptyTextureMemory -= usedBytes;
mTotalTextureMemory -= usedBytes;
pos = mSharedTextures.erase(pos);
}
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 SharedContextWebgl::ClearCachesIfNecessary() {
if (!mShouldClearCaches.exchange(
false)) {
return;
}
mZeroBuffer = nullptr;
ClearAllTextures();
if (mEmptyTextureMemory) {
ClearEmptyTextureMemory();
}
ClearLastTexture();
}
// 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,
const RefPtr<SharedContextWebgl>& aSharedContext) {
MOZ_ASSERT(format == SurfaceFormat::B8G8R8A8 ||
format == SurfaceFormat::B8G8R8X8);
mSize = size;
mFormat = format;
if (!aSharedContext || aSharedContext->IsContextLost() ||
aSharedContext->mDrawTargetCount >=
StaticPrefs::gfx_canvas_accelerated_max_draw_target_count()) {
return false;
}
mSharedContext = aSharedContext;
mSharedContext->mDrawTargetCount++;
if (size_t(std::max(size.width, size.height)) >
mSharedContext->mMaxTextureSize) {
return false;
}
if (!CreateFramebuffer()) {
return false;
}
size_t byteSize = layers::ImageDataSerializer::ComputeRGBBufferSize(
mSize, SurfaceFormat::B8G8R8A8);
if (byteSize == 0) {
return false;
}
size_t shmemSize = mozilla::ipc::SharedMemory::PageAlignedSize(byteSize);
if (NS_WARN_IF(shmemSize > UINT32_MAX)) {
MOZ_ASSERT_UNREACHABLE(
"Buffer too big?");
return false;
}
auto shmem = MakeRefPtr<mozilla::ipc::SharedMemory>();
if (NS_WARN_IF(!shmem->Create(shmemSize)) ||
NS_WARN_IF(!shmem->Map(shmemSize))) {
return false;
}
mShmem = std::move(shmem);
mShmemSize = shmemSize;
mSkia =
new DrawTargetSkia;
auto stride = layers::ImageDataSerializer::ComputeRGBStride(
SurfaceFormat::B8G8R8A8, size.width);
if (!mSkia->Init(
reinterpret_cast<uint8_t*>(mShmem->Memory()), size, stride,
SurfaceFormat::B8G8R8A8,
true)) {
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;
}
// If a non-recoverable error occurred that would stop the canvas from initing.
static Atomic<
bool> sContextInitError(
false);
already_AddRefed<SharedContextWebgl> SharedContextWebgl::Create() {
// If context initialization would fail, don't even try to create a context.
if (sContextInitError) {
return nullptr;
}
RefPtr<SharedContextWebgl> sharedContext =
new SharedContextWebgl;
if (!sharedContext->Initialize()) {
return nullptr;
}
return sharedContext.forget();
}
bool SharedContextWebgl::Initialize() {
WebGLContextOptions options = {};
options.alpha =
true;
options.depth =
false;
options.stencil =
false;
options.antialias =
false;
options.preserveDrawingBuffer =
true;
options.failIfMajorPerformanceCaveat =
false;
const bool resistFingerprinting = nsContentUtils::ShouldResistFingerprinting(
"Fallback", RFPTarget::WebGLRenderCapability);
const auto initDesc = webgl::InitContextDesc{
.isWebgl2 =
true,
.resistFingerprinting = resistFingerprinting,
.principalKey = 0,
.size = {1, 1},
.options = options,
};
webgl::InitContextResult initResult;
mWebgl = WebGLContext::Create(nullptr, initDesc, &initResult);
if (!mWebgl) {
// 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 = initResult.limits.maxTex2dSize;
if (kIsMacOS) {
mRasterizationTruncates = initResult.vendor == gl::GLVendor::ATI;
}
CachePrefs();
if (!CreateShaders()) {
// There was a non-recoverable error when trying to init shaders.
sContextInitError =
true;
mWebgl = nullptr;
return false;
}
mWGRPathBuilder = WGR::wgr_new_builder();
return true;
}
inline void SharedContextWebgl::BlendFunc(GLenum aSrcFactor,
GLenum aDstFactor) {
mWebgl->BlendFuncSeparate({}, aSrcFactor, aDstFactor, aSrcFactor, aDstFactor);
}
void SharedContextWebgl::SetBlendState(CompositionOp aOp,
const Maybe<DeviceColor>& 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.
// Map the composition op to a WebGL blend mode, if possible.
bool enabled =
true;
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);
BlendFunc(LOCAL_GL_CONSTANT_COLOR, LOCAL_GL_ONE_MINUS_SRC_COLOR);
}
else {
BlendFunc(LOCAL_GL_ONE, LOCAL_GL_ONE_MINUS_SRC_ALPHA);
}
break;
case CompositionOp::OP_ADD:
BlendFunc(LOCAL_GL_ONE, LOCAL_GL_ONE);
break;
case CompositionOp::OP_ATOP:
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);
BlendFunc(LOCAL_GL_CONSTANT_COLOR, LOCAL_GL_ONE_MINUS_SRC_COLOR);
}
else {
enabled =
false;
}
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:
enabled =
false;
break;
}
mWebgl->SetEnabled(LOCAL_GL_BLEND, {}, enabled);
}
// Ensure the WebGL framebuffer is set to the current target.
bool SharedContextWebgl::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);
}
}
return true;
}
// Replace the current clip rect with a new potentially-AA'd clip rect.
void SharedContextWebgl::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);
}
}
bool SharedContextWebgl::SetClipMask(
const RefPtr<WebGLTexture>& aTex) {
if (mLastClipMask != aTex) {
if (!mWebgl) {
return false;
}
mWebgl->ActiveTexture(1);
mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, aTex);
mWebgl->ActiveTexture(0);
mLastClipMask = aTex;
}
return true;
}
bool SharedContextWebgl::SetNoClipMask() {
if (mNoClipMask) {
return SetClipMask(mNoClipMask);
}
if (!mWebgl) {
return false;
}
mNoClipMask = mWebgl->CreateTexture();
if (!mNoClipMask) {
return false;
}
mWebgl->ActiveTexture(1);
mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, mNoClipMask);
static const auto solidMask =
std::array<
const uint8_t, 4>{0xFF, 0xFF, 0xFF, 0xFF};
mWebgl->TexImage(0, LOCAL_GL_RGBA8, {0, 0, 0},
{LOCAL_GL_RGBA, LOCAL_GL_UNSIGNED_BYTE},
{LOCAL_GL_TEXTURE_2D,
{1, 1, 1},
gfxAlphaType::NonPremult,
Some(Span{solidMask})});
InitTexParameters(mNoClipMask,
false);
mWebgl->ActiveTexture(0);
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<
const PathSkia*>(mPath.get())->GetPath() ==
static_cast<
const PathSkia*>(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<WebGLContext> 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<IntRect> 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<DrawTargetSkia> 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. This is done on texture unit 0 so that
// we can work around an Windows Intel driver bug. If done on texture unit 1,
// the driver doesn't notice that the texture contents was modified. Force a
// re-latch by binding the texture on texture unit 1 only after modification.
webgl->BindTexture(LOCAL_GL_TEXTURE_2D, mClipMask);
if (init) {
mSharedContext->InitTexParameters(mClipMask,
false);
}
RefPtr<DataSourceSurface> data;
if (RefPtr<SourceSurface> 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);
mSharedContext->ClearLastTexture();
// Bind the new clip mask to the clip sampler on texture unit 1.
mSharedContext->SetClipMask(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<IntRect> 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 SharedContextWebgl::IsContextLost()
const {
return !mWebgl || mWebgl->IsContextLost();
}
// Signal to CanvasRenderingContext2D when the WebGL context is lost.
bool DrawTargetWebgl::IsValid()
const {
return mSharedContext && !mSharedContext->IsContextLost();
}
bool DrawTargetWebgl::CanCreate(
const IntSize& aSize, SurfaceFormat aFormat) {
if (!gfxVars::UseAcceleratedCanvas2D()) {
return false;
}
if (!Factory::AllowedSurfaceSize(aSize)) {
return false;
}
// 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 false;
}
int32_t minSize = StaticPrefs::gfx_canvas_accelerated_min_size();
if (aSize.width * aSize.height < minSize * minSize) {
return false;
}
// 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 false;
}
}
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;
if (RefPtr<widget::Screen> screen =
widget::ScreenManager::GetSingleton().GetPrimaryScreen()) {
LayoutDeviceIntSize screenSize = screen->GetRect().Size();
if (aSize.width * aSize.height >
std::max(screenSize.width * screenSize.height, kScreenPixels)) {
return false;
}
}
}
return true;
}
already_AddRefed<DrawTargetWebgl> DrawTargetWebgl::Create(
const IntSize& aSize, SurfaceFormat aFormat,
const RefPtr<SharedContextWebgl>& aSharedContext) {
// Validate the size and format.
if (!CanCreate(aSize, aFormat)) {
return nullptr;
}
RefPtr<DrawTargetWebgl> dt =
new DrawTargetWebgl;
if (!dt->Init(aSize, aFormat, aSharedContext) || !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<TextureHandle> SharedContextWebgl::WrapSnapshot(
const IntSize& aSize, SurfaceFormat aFormat, RefPtr<WebGLTexture> aTex) {
// Ensure there is enough space for the texture.
size_t usedBytes = BackingTexture::UsedBytes(aFormat, aSize);
PruneTextureMemory(usedBytes,
false);
// Allocate a handle for the texture
RefPtr<StandaloneTexture> handle =
new StandaloneTexture(aSize, aFormat, aTex.forget());
mStandaloneTextures.push_back(handle);
mTextureHandles.insertFront(handle);
mTotalTextureMemory += usedBytes;
mUsedTextureMemory += usedBytes;
++mNumTextureHandles;
return handle.forget();
}
void SharedContextWebgl::SetTexFilter(WebGLTexture* aTex,
bool aFilter) {
mWebgl->TexParameter_base(
LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MAG_FILTER,
FloatOrInt(aFilter ? LOCAL_GL_LINEAR : LOCAL_GL_NEAREST));
mWebgl->TexParameter_base(
LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_MIN_FILTER,
FloatOrInt(aFilter ? LOCAL_GL_LINEAR : LOCAL_GL_NEAREST));
}
void SharedContextWebgl::InitTexParameters(WebGLTexture* aTex,
bool aFilter) {
mWebgl->TexParameter_base(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_S,
FloatOrInt(LOCAL_GL_REPEAT));
mWebgl->TexParameter_base(LOCAL_GL_TEXTURE_2D, LOCAL_GL_TEXTURE_WRAP_T,
FloatOrInt(LOCAL_GL_REPEAT));
SetTexFilter(aTex, aFilter);
}
// Copy the contents of the WebGL framebuffer into a WebGL texture.
already_AddRefed<TextureHandle> SharedContextWebgl::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<WebGLTexture> 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);
webgl::FbAttachInfo attachInfo;
attachInfo.tex = aHandle->GetBackingTexture()->GetWebGLTexture();
mWebgl->FramebufferAttach(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
LOCAL_GL_TEXTURE_2D, attachInfo);
}
// Create a texture to hold the copy
mWebgl->BindTexture(LOCAL_GL_TEXTURE_2D, tex);
mWebgl->TexStorage(LOCAL_GL_TEXTURE_2D, 1, LOCAL_GL_RGBA8,
{uint32_t(aRect.width), uint32_t(aRect.height), 1});
InitTexParameters(tex);
// Copy the framebuffer into the texture
mWebgl->CopyTexImage(LOCAL_GL_TEXTURE_2D, 0, 0, {0, 0, 0}, {aRect.x, aRect.y},
{uint32_t(aRect.width), uint32_t(aRect.height)});
ClearLastTexture();
SurfaceFormat format =
aHandle ? aHandle->GetFormat() : mCurrentTarget->GetFormat();
already_AddRefed<TextureHandle> 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<TextureHandle> DrawTargetWebgl::CopySnapshot(
const IntRect& aRect) {
AutoRestoreContext restore(
this);
if (!PrepareContext(
false)) {
return nullptr;
}
return mSharedContext->CopySnapshot(aRect);
}
bool DrawTargetWebgl::HasDataSnapshot()
const {
return (mSkiaValid && !mSkiaLayer) || (mSnapshot && mSnapshot->HasReadData());
}
bool DrawTargetWebgl::PrepareSkia() {
if (!mSkiaValid) {
ReadIntoSkia();
}
else if (mSkiaLayer) {
FlattenSkia();
}
return mSkiaValid;
}
bool DrawTargetWebgl::EnsureDataSnapshot() {
return HasDataSnapshot() || PrepareSkia();
}
void DrawTargetWebgl::PrepareShmem() { PrepareSkia(); }
// Borrow a snapshot that may be used by another thread for composition. Only
// Skia snapshots are safe to pass around.
already_AddRefed<SourceSurface> DrawTargetWebgl::GetDataSnapshot() {
PrepareSkia();
return mSkia->Snapshot(mFormat);
}
already_AddRefed<SourceSurface> 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<SourceSurface> DrawTargetWebgl::GetOptimizedSnapshot(
DrawTarget* aTarget) {
if (aTarget && aTarget->GetBackendType() == BackendType::WEBGL &&
static_cast<DrawTargetWebgl*>(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 SharedContextWebgl::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);
webgl::FbAttachInfo attachInfo;
attachInfo.tex = aHandle->GetBackingTexture()->GetWebGLTexture();
mWebgl->FramebufferAttach(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
LOCAL_GL_TEXTURE_2D, attachInfo);
}
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;
Range<uint8_t> range = {aDstData, size_t(aDstStride) * aBounds.height};
mWebgl->ReadPixelsInto(desc, range);
// Restore the actual framebuffer after reading is done.
if (aHandle && mCurrentTarget) {
mWebgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mCurrentTarget->mFramebuffer);
}
return true;
}
already_AddRefed<DataSourceSurface> SharedContextWebgl::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<DataSourceSurface> 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<DataSourceSurface> DrawTargetWebgl::ReadSnapshot() {
AutoRestoreContext restore(
this);
if (!PrepareContext(
false)) {
return nullptr;
}
mProfile.OnReadback();
return mSharedContext->ReadSnapshot();
}
already_AddRefed<SourceSurface> 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;
}
void DrawTargetWebgl::MarkSkiaChanged(
bool aOverwrite) {
if (aOverwrite) {
mSkiaValid =
true;
mSkiaLayer =
false;
}
else if (!mSkiaValid) {
if (ReadIntoSkia()) {
// Signal that we've hit a complete software fallback.
mProfile.OnFallback();
}
}
else if (mSkiaLayer) {
FlattenSkia();
}
mWebglValid =
false;
mIsClear =
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;
}
}
void DrawTargetWebgl::MarkSkiaChanged(
const DrawOptions& aOptions) {
if (SupportsLayering(aOptions)) {
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();
}
}
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 SharedContextWebgl::ResetPathVertexBuffer(
bool aChanged) {
mWebgl->BindBuffer(LOCAL_GL_ARRAY_BUFFER, mPathVertexBuffer.get());
mWebgl->UninitializedBufferData_SizeOnly(
LOCAL_GL_ARRAY_BUFFER,
std::max(size_t(mPathVertexCapacity),
sizeof(kRectVertexData)),
LOCAL_GL_DYNAMIC_DRAW);
mWebgl->BufferSubData(LOCAL_GL_ARRAY_BUFFER, 0,
sizeof(kRectVertexData),
(
const uint8_t*)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 SharedContextWebgl::CreateShaders() {
if (!mPathVertexArray) {
mPathVertexArray = mWebgl->CreateVertexArray();
}
if (!mPathVertexBuffer) {
mPathVertexBuffer = mWebgl->CreateBuffer();
mWebgl->BindVertexArray(mPathVertexArray.get());
ResetPathVertexBuffer();
mWebgl->EnableVertexAttribArray(0);
webgl::VertAttribPointerDesc attribDesc;
attribDesc.channels = 3;
attribDesc.type = LOCAL_GL_FLOAT;
attribDesc.normalized =
false;
mWebgl->VertexAttribPointer(0, attribDesc);
}
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 =
"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 +\n"
" 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"
" float noAA = 1.0 - u_aa;\n"
" v_dist = vec4(extrude, 1.0 - extrude) * scale.xyxy + 0.5 + noAA;\n"
" v_alpha = min(a_vertex.z,\n"
" min(scale.x, 1.0) * min(scale.y, 1.0) + noAA);\n"
"}\n";
auto fsSource =
"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 = clamp(min(dist.x, dist.y), 0.0, v_alpha);\n"
" gl_FragColor = clip * aa * u_color;\n"
"}\n";
RefPtr<WebGLShader> vsId = mWebgl->CreateShader(LOCAL_GL_VERTEX_SHADER);
mWebgl->ShaderSource(*vsId, vsSource);
mWebgl->CompileShader(*vsId);
if (!mWebgl->GetCompileResult(*vsId).success) {
return false;
}
RefPtr<WebGLShader> 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,
"a_vertex");
mWebgl->LinkProgram(*mSolidProgram);
if (!mWebgl->GetLinkResult(*mSolidProgram).success) {
return false;
}
mSolidProgramViewport = GetUniformLocation(mSolidProgram,
"u_viewport");
mSolidProgramAA = GetUniformLocation(mSolidProgram,
"u_aa");
mSolidProgramTransform = GetUniformLocation(mSolidProgram,
"u_transform");
mSolidProgramColor = GetUniformLocation(mSolidProgram,
"u_color");
mSolidProgramClipMask = GetUniformLocation(mSolidProgram,
"u_clipmask");
mSolidProgramClipBounds = GetUniformLocation(mSolidProgram,
"u_clipbounds");
if (!mSolidProgramViewport || !mSolidProgramAA || !mSolidProgramTransform ||
!mSolidProgramColor || !mSolidProgramClipMask ||
!mSolidProgramClipBounds) {
return false;
}
mWebgl->UseProgram(mSolidProgram);
UniformData(LOCAL_GL_INT, mSolidProgramClipMask, Array<int32_t, 1>{1});
}
if (!mImageProgram) {
auto vsSource =
"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 +\n"
" 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"
" float noAA = 1.0 - u_aa;\n"
" v_dist = vec4(extrude, 1.0 - extrude) * scale.xyxy + 0.5 + noAA;\n"
" v_alpha = min(a_vertex.z,\n"
" min(scale.x, 1.0) * min(scale.y, 1.0) + noAA);\n"
"}\n";
auto fsSource =
"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 = clamp(min(dist.x, dist.y), 0.0, v_alpha);\n"
" gl_FragColor = clip * aa * u_color *\n"
" mix(image, image.rrrr, u_swizzle);\n"
"}\n";
RefPtr<WebGLShader> vsId = mWebgl->CreateShader(LOCAL_GL_VERTEX_SHADER);
mWebgl->ShaderSource(*vsId, vsSource);
mWebgl->CompileShader(*vsId);
if (!mWebgl->GetCompileResult(*vsId).success) {
return false;
}
RefPtr<WebGLShader> 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,
"a_vertex");
mWebgl->LinkProgram(*mImageProgram);
if (!mWebgl->GetLinkResult(*mImageProgram).success) {
return false;
}
mImageProgramViewport = GetUniformLocation(mImageProgram,
"u_viewport");
mImageProgramAA = GetUniformLocation(mImageProgram,
"u_aa");
mImageProgramTransform = GetUniformLocation(mImageProgram,
"u_transform");
mImageProgramTexMatrix = GetUniformLocation(mImageProgram,
"u_texmatrix");
mImageProgramTexBounds = GetUniformLocation(mImageProgram,
"u_texbounds");
mImageProgramSwizzle = GetUniformLocation(mImageProgram,
"u_swizzle");
mImageProgramColor = GetUniformLocation(mImageProgram,
"u_color");
mImageProgramSampler = GetUniformLocation(mImageProgram,
"u_sampler");
mImageProgramClipMask = GetUniformLocation(mImageProgram,
"u_clipmask");
mImageProgramClipBounds = GetUniformLocation(mImageProgram,
"u_clipbounds");
if (!mImageProgramViewport || !mImageProgramAA || !mImageProgramTransform ||
!mImageProgramTexMatrix || !mImageProgramTexBounds ||
!mImageProgramSwizzle || !mImageProgramColor || !mImageProgramSampler ||
!mImageProgramClipMask || !mImageProgramClipBounds) {
return false;
}
mWebgl->UseProgram(mImageProgram);
UniformData(LOCAL_GL_INT, mImageProgramSampler, Array<int32_t, 1>{0});
UniformData(LOCAL_GL_INT, mImageProgramClipMask, Array<int32_t, 1>{1});
}
return true;
}
void SharedContextWebgl::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->SetEnabled(LOCAL_GL_SCISSOR_TEST, {},
true);
}
}
void SharedContextWebgl::DisableScissor() {
if (mScissorEnabled) {
mScissorEnabled =
false;
mWebgl->SetEnabled(LOCAL_GL_SCISSOR_TEST, {},
false);
}
}
inline ColorPattern DrawTargetWebgl::GetClearPattern()
const {
return ColorPattern(
DeviceColor(0.0f, 0.0f, 0.0f, IsOpaque(mFormat) ? 1.0f : 0.0f));
}
template <
typename R>
inline RectDouble DrawTargetWebgl::TransformDouble(
const R& aRect)
const {
return MatrixDouble(mTransform).TransformBounds(WidenToDouble(aRect));
}
// Check if the transformed rect clips to the viewport.
inline Maybe<Rect> DrawTargetWebgl::RectClippedToViewport(
const RectDouble& aRect)
const {
if (!mTransform.PreservesAxisAlignedRectangles()) {
return Nothing();
}
return Some(NarrowToFloat(aRect.SafeIntersect(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.
template <
typename R>
static inline bool RectInsidePrecisionLimits(
const R& aRect) {
return R(-(1 << 20), -(1 << 20), 2 << 20, 2 << 20).Contains(aRect);
}
void DrawTargetWebgl::ClearRect(
const Rect& aRect) {
if (mIsClear) {
// No need to clear anything if the entire framebuffer is already clear.
return;
}
RectDouble xformRect = TransformDouble(aRect);
bool containsViewport =
false;
if (Maybe<Rect> clipped = RectClippedToViewport(xformRect)) {
// If the rect clips to viewport, just clear the clipped rect
// to avoid transform issues.
containsViewport = clipped->Size() == Size(GetSize());
DrawRect(*clipped, GetClearPattern(),
DrawOptions(1.0f, CompositionOp::OP_CLEAR), Nothing(), nullptr,
false);
}
else if (RectInsidePrecisionLimits(xformRect)) {
// 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<WebGLContext> webgl = mSharedContext->mWebgl;
if (!mFramebuffer) {
mFramebuffer = webgl->CreateFramebuffer();
}
if (!mTex) {
mTex = webgl->CreateTexture();
webgl->BindTexture(LOCAL_GL_TEXTURE_2D, mTex);
webgl->TexStorage(LOCAL_GL_TEXTURE_2D, 1, LOCAL_GL_RGBA8,
{uint32_t(mSize.width), uint32_t(mSize.height), 1});
mSharedContext->InitTexParameters(mTex);
webgl->BindFramebuffer(LOCAL_GL_FRAMEBUFFER, mFramebuffer);
webgl::FbAttachInfo attachInfo;
attachInfo.tex = mTex;
webgl->FramebufferAttach(LOCAL_GL_FRAMEBUFFER, LOCAL_GL_COLOR_ATTACHMENT0,
LOCAL_GL_TEXTURE_2D, attachInfo);
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.
if (Maybe<Rect> rect = aPath->AsRect()) {
PushClipRect(*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;
}
bool DrawTargetWebgl::CopyToFallback(DrawTarget* aDT) {
aDT->RemoveAllClips();
for (
auto& clipStack : mClipStack) {
aDT->SetTransform(clipStack.mTransform);
if (clipStack.mPath) {
aDT->PushClip(clipStack.mPath);
}
else {
aDT->PushClipRect(clipStack.mRect);
}
}
aDT->SetTransform(GetTransform());
// An existing data snapshot is required for fallback, as we have to avoid
// trying to touch the WebGL context, which is assumed to be invalid and not
// suitable for readback.
if (HasDataSnapshot()) {
if (RefPtr<SourceSurface> snapshot = Snapshot()) {
aDT->CopySurface(snapshot, snapshot->GetRect(), gfx::IntPoint(0, 0));
return true;
}
}
return false;
}
// 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;
}
}
static inline bool SupportsExtendMode(
const SurfacePattern& aPattern) {
switch (aPattern.mExtendMode) {
case ExtendMode::CLAMP:
return true;
case ExtendMode::REPEAT:
case ExtendMode::REPEAT_X:
case ExtendMode::REPEAT_Y:
if ((!aPattern.mSurface ||
aPattern.mSurface->GetType() == SurfaceType::WEBGL) &&
!aPattern.mSamplingRect.IsEmpty()) {
return false;
}
return true;
default:
return false;
}
}
// Whether a pattern can be mapped to an available WebGL shader.
bool SharedContextWebgl::SupportsPattern(
const Pattern& aPattern) {
switch (aPattern.GetType()) {
case PatternType::COLOR:
return true;
case PatternType::SURFACE: {
auto surfacePattern =
static_cast<
const SurfacePattern&>(aPattern);
if (!SupportsExtendMode(surfacePattern)) {
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;
}
}
bool DrawTargetWebgl::DrawRect(
const Rect& aRect,
const Pattern& aPattern,
const DrawOptions& aOptions,
Maybe<DeviceColor> aMaskColor,
RefPtr<TextureHandle>* 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<DeviceColor> 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<
const SurfacePattern&>(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<
const SurfacePattern&>(aPattern);
--> --------------------
--> maximum size reached
--> --------------------