Files
tubestation/gfx/layers/wr/AsyncImagePipelineManager.cpp
Chris Pearce c6e51d4e51 Bug 1434503 - Round video frame images bounds to device pixel boundaries before passing to WebRender. r=sotaro
When scrolling on Twitter video frames can sometimes be drawn with a flicker
above or below them. This is because the bounds for video frames' images that
we pass to WebRender are not rounded to pixel boundaries, and so they can end
up being rounded off in WebRender unexpectedly when composited.

So round off the bounds of video frame images to device pixel boundaries before
passing to WebRender.

Differential Revision: https://phabricator.services.mozilla.com/D6857
2018-09-26 00:18:21 +00:00

651 lines
22 KiB
C++

/* -*- 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 "AsyncImagePipelineManager.h"
#include "CompositableHost.h"
#include "gfxEnv.h"
#include "mozilla/gfx/gfxVars.h"
#include "mozilla/layers/CompositorThread.h"
#include "mozilla/layers/SharedSurfacesParent.h"
#include "mozilla/layers/WebRenderImageHost.h"
#include "mozilla/layers/WebRenderTextureHost.h"
#include "mozilla/webrender/RenderThread.h"
#include "mozilla/webrender/WebRenderAPI.h"
#include "mozilla/webrender/WebRenderTypes.h"
namespace mozilla {
namespace layers {
AsyncImagePipelineManager::AsyncImagePipeline::AsyncImagePipeline()
: mInitialised(false)
, mIsChanged(false)
, mUseExternalImage(false)
, mFilter(wr::ImageRendering::Auto)
, mMixBlendMode(wr::MixBlendMode::Normal)
{}
AsyncImagePipelineManager::AsyncImagePipelineManager(already_AddRefed<wr::WebRenderAPI>&& aApi)
: mApi(aApi)
, mIdNamespace(mApi->GetNamespace())
, mResourceId(0)
, mAsyncImageEpoch{0}
, mWillGenerateFrame(false)
, mDestroyed(false)
, mUpdatesLock("UpdatesLock")
{
MOZ_COUNT_CTOR(AsyncImagePipelineManager);
}
AsyncImagePipelineManager::~AsyncImagePipelineManager()
{
MOZ_COUNT_DTOR(AsyncImagePipelineManager);
}
void
AsyncImagePipelineManager::Destroy()
{
MOZ_ASSERT(!mDestroyed);
mApi = nullptr;
mDestroyed = true;
}
void
AsyncImagePipelineManager::SetWillGenerateFrame()
{
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
mWillGenerateFrame = true;
}
bool
AsyncImagePipelineManager::GetAndResetWillGenerateFrame()
{
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
bool ret = mWillGenerateFrame;
mWillGenerateFrame = false;
return ret;
}
wr::ExternalImageId
AsyncImagePipelineManager::GetNextExternalImageId()
{
static uint32_t sNextId = 0;
++sNextId;
MOZ_RELEASE_ASSERT(sNextId != UINT32_MAX);
// gecko allocates external image id as (IdNamespace:32bit + ResourceId:32bit).
// And AsyncImagePipelineManager uses IdNamespace = 0.
return wr::ToExternalImageId((uint64_t)sNextId);
}
void
AsyncImagePipelineManager::AddPipeline(const wr::PipelineId& aPipelineId)
{
if (mDestroyed) {
return;
}
uint64_t id = wr::AsUint64(aPipelineId);
PipelineTexturesHolder* holder = mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
if(holder) {
// This could happen during tab move between different windows.
// Previously removed holder could be still alive for waiting destroyed.
MOZ_ASSERT(holder->mDestroyedEpoch.isSome());
holder->mDestroyedEpoch = Nothing(); // Revive holder
return;
}
holder = new PipelineTexturesHolder();
mPipelineTexturesHolders.Put(id, holder);
}
void
AsyncImagePipelineManager::RemovePipeline(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch)
{
if (mDestroyed) {
return;
}
PipelineTexturesHolder* holder = mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
MOZ_ASSERT(holder);
if (!holder) {
return;
}
holder->mDestroyedEpoch = Some(aEpoch);
}
void
AsyncImagePipelineManager::AddAsyncImagePipeline(const wr::PipelineId& aPipelineId, WebRenderImageHost* aImageHost)
{
if (mDestroyed) {
return;
}
MOZ_ASSERT(aImageHost);
uint64_t id = wr::AsUint64(aPipelineId);
MOZ_ASSERT(!mAsyncImagePipelines.Get(id));
AsyncImagePipeline* holder = new AsyncImagePipeline();
holder->mImageHost = aImageHost;
mAsyncImagePipelines.Put(id, holder);
AddPipeline(aPipelineId);
}
void
AsyncImagePipelineManager::RemoveAsyncImagePipeline(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn)
{
if (mDestroyed) {
return;
}
uint64_t id = wr::AsUint64(aPipelineId);
if (auto entry = mAsyncImagePipelines.Lookup(id)) {
AsyncImagePipeline* holder = entry.Data();
wr::Epoch epoch = GetNextImageEpoch();
aTxn.ClearDisplayList(epoch, aPipelineId);
for (wr::ImageKey key : holder->mKeys) {
aTxn.DeleteImage(key);
}
entry.Remove();
RemovePipeline(aPipelineId, epoch);
}
}
void
AsyncImagePipelineManager::UpdateAsyncImagePipeline(const wr::PipelineId& aPipelineId,
const LayoutDeviceRect& aScBounds,
const gfx::Matrix4x4& aScTransform,
const gfx::MaybeIntSize& aScaleToSize,
const wr::ImageRendering& aFilter,
const wr::MixBlendMode& aMixBlendMode)
{
if (mDestroyed) {
return;
}
AsyncImagePipeline* pipeline = mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
if (!pipeline) {
return;
}
pipeline->mInitialised = true;
pipeline->Update(aScBounds,
aScTransform,
aScaleToSize,
aFilter,
aMixBlendMode);
}
Maybe<TextureHost::ResourceUpdateOp>
AsyncImagePipelineManager::UpdateImageKeys(const wr::Epoch& aEpoch,
const wr::PipelineId& aPipelineId,
AsyncImagePipeline* aPipeline,
nsTArray<wr::ImageKey>& aKeys,
wr::TransactionBuilder& aSceneBuilderTxn,
wr::TransactionBuilder& aMaybeFastTxn)
{
MOZ_ASSERT(aKeys.IsEmpty());
MOZ_ASSERT(aPipeline);
TextureHost* texture = aPipeline->mImageHost->GetAsTextureHostForComposite();
TextureHost* previousTexture = aPipeline->mCurrentTexture.get();
if (texture == previousTexture) {
// The texture has not changed, just reuse previous ImageKeys.
aKeys = aPipeline->mKeys;
if (aPipeline->mWrTextureWrapper) {
HoldExternalImage(aPipelineId, aEpoch, aPipeline->mWrTextureWrapper);
}
return Nothing();
}
if (!texture) {
// We don't have a new texture, there isn't much we can do.
aKeys = aPipeline->mKeys;
if (aPipeline->mWrTextureWrapper) {
HoldExternalImage(aPipelineId, aEpoch, aPipeline->mWrTextureWrapper);
}
return Nothing();
}
aPipeline->mCurrentTexture = texture;
WebRenderTextureHost* wrTexture = texture->AsWebRenderTextureHost();
bool useExternalImage = !gfxEnv::EnableWebRenderRecording() && wrTexture;
aPipeline->mUseExternalImage = useExternalImage;
// Use WebRenderTextureHostWrapper only for video.
// And WebRenderTextureHostWrapper could be used only with WebRenderTextureHost
// that supports NativeTexture
bool useWrTextureWrapper = aPipeline->mImageHost->GetAsyncRef() &&
useExternalImage &&
wrTexture &&
wrTexture->SupportsWrNativeTexture();
// The non-external image code path falls back to converting the texture into
// an rgb image.
auto numKeys = useExternalImage ? texture->NumSubTextures() : 1;
// If we already had a texture and the format hasn't changed, better to reuse the image keys
// than create new ones.
bool canUpdate = !!previousTexture
&& previousTexture->GetSize() == texture->GetSize()
&& previousTexture->GetFormat() == texture->GetFormat()
&& aPipeline->mKeys.Length() == numKeys;
// Check if WebRenderTextureHostWrapper could be reused.
if (aPipeline->mWrTextureWrapper &&
(!useWrTextureWrapper || !canUpdate)) {
aPipeline->mWrTextureWrapper = nullptr;
canUpdate = false;
}
if (!canUpdate) {
for (auto key : aPipeline->mKeys) {
// Destroy ImageKeys on transaction of scene builder thread, since DisplayList is
// updated on SceneBuilder thread. It prevents too early ImageKey deletion.
aSceneBuilderTxn.DeleteImage(key);
}
aPipeline->mKeys.Clear();
for (uint32_t i = 0; i < numKeys; ++i) {
aPipeline->mKeys.AppendElement(GenerateImageKey());
}
}
aKeys = aPipeline->mKeys;
auto op = canUpdate ? TextureHost::UPDATE_IMAGE : TextureHost::ADD_IMAGE;
if (!useExternalImage) {
return UpdateWithoutExternalImage(texture, aKeys[0], op, aMaybeFastTxn);
}
if (useWrTextureWrapper && aPipeline->mWrTextureWrapper) {
MOZ_ASSERT(canUpdate);
// Reuse WebRenderTextureHostWrapper. With it, rendered frame could be updated
// without batch re-creation.
aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(wrTexture);
// Ensure frame generation.
SetWillGenerateFrame();
} else {
if (useWrTextureWrapper) {
aPipeline->mWrTextureWrapper = new WebRenderTextureHostWrapper(this);
aPipeline->mWrTextureWrapper->UpdateWebRenderTextureHost(wrTexture);
}
Range<wr::ImageKey> keys(&aKeys[0], aKeys.Length());
auto externalImageKey =
aPipeline->mWrTextureWrapper ? aPipeline->mWrTextureWrapper->GetExternalImageKey() : wrTexture->GetExternalImageKey();
wrTexture->PushResourceUpdates(aMaybeFastTxn, op, keys, externalImageKey);
}
if (aPipeline->mWrTextureWrapper) {
HoldExternalImage(aPipelineId, aEpoch, aPipeline->mWrTextureWrapper);
}
return Some(op);
}
Maybe<TextureHost::ResourceUpdateOp>
AsyncImagePipelineManager::UpdateWithoutExternalImage(TextureHost* aTexture,
wr::ImageKey aKey,
TextureHost::ResourceUpdateOp aOp,
wr::TransactionBuilder& aTxn)
{
MOZ_ASSERT(aTexture);
RefPtr<gfx::DataSourceSurface> dSurf = aTexture->GetAsSurface();
if (!dSurf) {
NS_ERROR("TextureHost does not return DataSourceSurface");
return Nothing();
}
gfx::DataSourceSurface::MappedSurface map;
if (!dSurf->Map(gfx::DataSourceSurface::MapType::READ, &map)) {
NS_ERROR("DataSourceSurface failed to map");
return Nothing();
}
gfx::IntSize size = dSurf->GetSize();
wr::ImageDescriptor descriptor(size, map.mStride, dSurf->GetFormat());
// Costly copy right here...
wr::Vec<uint8_t> bytes;
bytes.PushBytes(Range<uint8_t>(map.mData, size.height * map.mStride));
if (aOp == TextureHost::UPDATE_IMAGE) {
aTxn.UpdateImageBuffer(aKey, descriptor, bytes);
} else {
aTxn.AddImage(aKey, descriptor, bytes);
}
dSurf->Unmap();
return Some(aOp);
}
void
AsyncImagePipelineManager::ApplyAsyncImagesOfImageBridge(wr::TransactionBuilder& aSceneBuilderTxn,
wr::TransactionBuilder& aFastTxn)
{
if (mDestroyed || mAsyncImagePipelines.Count() == 0) {
return;
}
wr::Epoch epoch = GetNextImageEpoch();
// We use a pipeline with a very small display list for each video element.
// Update each of them if needed.
for (auto iter = mAsyncImagePipelines.Iter(); !iter.Done(); iter.Next()) {
wr::PipelineId pipelineId = wr::AsPipelineId(iter.Key());
AsyncImagePipeline* pipeline = iter.Data();
// If aync image pipeline does not use ImageBridge, do not need to apply.
if (!pipeline->mImageHost->GetAsyncRef()) {
continue;
}
ApplyAsyncImageForPipeline(epoch, pipelineId, pipeline, aSceneBuilderTxn, aFastTxn);
}
}
void
AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::Epoch& aEpoch,
const wr::PipelineId& aPipelineId,
AsyncImagePipeline* aPipeline,
wr::TransactionBuilder& aSceneBuilderTxn,
wr::TransactionBuilder& aMaybeFastTxn)
{
nsTArray<wr::ImageKey> keys;
auto op = UpdateImageKeys(aEpoch, aPipelineId, aPipeline, keys, aSceneBuilderTxn, aMaybeFastTxn);
bool updateDisplayList = aPipeline->mInitialised &&
(aPipeline->mIsChanged || op == Some(TextureHost::ADD_IMAGE)) &&
!!aPipeline->mCurrentTexture;
if (!updateDisplayList) {
// We don't need to update the display list, either because we can't or because
// the previous one is still up to date.
// We may, however, have updated some resources.
// Use transaction of scene builder thread to notify epoch.
// It is for making epoch update consistent.
aSceneBuilderTxn.UpdateEpoch(aPipelineId, aEpoch);
if (aPipeline->mCurrentTexture) {
HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture->AsWebRenderTextureHost());
}
return;
}
aPipeline->mIsChanged = false;
wr::LayoutSize contentSize { aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height() };
wr::DisplayListBuilder builder(aPipelineId, contentSize);
float opacity = 1.0f;
Maybe<wr::WrClipId> referenceFrameId = builder.PushStackingContext(
wr::ToRoundedLayoutRect(aPipeline->mScBounds),
nullptr,
nullptr,
&opacity,
aPipeline->mScTransform.IsIdentity() ? nullptr : &aPipeline->mScTransform,
wr::TransformStyle::Flat,
nullptr,
aPipeline->mMixBlendMode,
nsTArray<wr::WrFilterOp>(),
true,
// This is fine to do unconditionally because we only push images here.
wr::RasterSpace::Screen());
if (aPipeline->mCurrentTexture && !keys.IsEmpty()) {
LayoutDeviceRect rect(0, 0, aPipeline->mCurrentTexture->GetSize().width, aPipeline->mCurrentTexture->GetSize().height);
if (aPipeline->mScaleToSize.isSome()) {
rect = LayoutDeviceRect(0, 0, aPipeline->mScaleToSize.value().width, aPipeline->mScaleToSize.value().height);
}
if (aPipeline->mUseExternalImage) {
MOZ_ASSERT(aPipeline->mCurrentTexture->AsWebRenderTextureHost());
Range<wr::ImageKey> range_keys(&keys[0], keys.Length());
aPipeline->mCurrentTexture->PushDisplayItems(builder,
wr::ToRoundedLayoutRect(rect),
wr::ToRoundedLayoutRect(rect),
aPipeline->mFilter,
range_keys);
HoldExternalImage(aPipelineId, aEpoch, aPipeline->mCurrentTexture->AsWebRenderTextureHost());
} else {
MOZ_ASSERT(keys.Length() == 1);
builder.PushImage(wr::ToRoundedLayoutRect(rect),
wr::ToRoundedLayoutRect(rect),
true,
aPipeline->mFilter,
keys[0]);
}
}
builder.PopStackingContext(referenceFrameId.isSome());
wr::BuiltDisplayList dl;
wr::LayoutSize builderContentSize;
builder.Finalize(builderContentSize, dl);
aSceneBuilderTxn.SetDisplayList(
gfx::Color(0.f, 0.f, 0.f, 0.f),
aEpoch,
LayerSize(aPipeline->mScBounds.Width(), aPipeline->mScBounds.Height()),
aPipelineId, builderContentSize,
dl.dl_desc, dl.dl);
}
void
AsyncImagePipelineManager::ApplyAsyncImageForPipeline(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aSceneBuilderTxn)
{
AsyncImagePipeline* pipeline = mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
if (!pipeline) {
return;
}
wr::TransactionBuilder fastTxn(/* aUseSceneBuilderThread */ false);
wr::AutoTransactionSender sender(mApi, &fastTxn);
// Use transaction of using non scene builder thread when ImageHost uses ImageBridge.
// ApplyAsyncImagesOfImageBridge() handles transaction of adding and updating
// wr::ImageKeys of ImageHosts that uses ImageBridge. Then AsyncImagePipelineManager
// always needs to use non scene builder thread transaction for adding and updating
// wr::ImageKeys of ImageHosts that uses ImageBridge. Otherwise, ordering of
// wr::ImageKeys updating in webrender becomes inconsistent.
auto& txn = pipeline->mImageHost->GetAsyncRef() ? fastTxn : aSceneBuilderTxn;
wr::Epoch epoch = GetNextImageEpoch();
ApplyAsyncImageForPipeline(epoch, aPipelineId, pipeline, aSceneBuilderTxn, txn);
}
void
AsyncImagePipelineManager::SetEmptyDisplayList(const wr::PipelineId& aPipelineId, wr::TransactionBuilder& aTxn)
{
AsyncImagePipeline* pipeline = mAsyncImagePipelines.Get(wr::AsUint64(aPipelineId));
if (!pipeline) {
return;
}
wr::Epoch epoch = GetNextImageEpoch();
wr::LayoutSize contentSize { pipeline->mScBounds.Width(), pipeline->mScBounds.Height() };
wr::DisplayListBuilder builder(aPipelineId, contentSize);
wr::BuiltDisplayList dl;
wr::LayoutSize builderContentSize;
builder.Finalize(builderContentSize, dl);
aTxn.SetDisplayList(gfx::Color(0.f, 0.f, 0.f, 0.f),
epoch,
LayerSize(pipeline->mScBounds.Width(), pipeline->mScBounds.Height()),
aPipelineId, builderContentSize,
dl.dl_desc, dl.dl);
}
void
AsyncImagePipelineManager::HoldExternalImage(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch, WebRenderTextureHost* aTexture)
{
if (mDestroyed) {
return;
}
MOZ_ASSERT(aTexture);
PipelineTexturesHolder* holder = mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
MOZ_ASSERT(holder);
if (!holder) {
return;
}
// Hold WebRenderTextureHost until end of its usage on RenderThread
holder->mTextureHosts.push(ForwardingTextureHost(aEpoch, aTexture));
}
void
AsyncImagePipelineManager::HoldExternalImage(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch, WebRenderTextureHostWrapper* aWrTextureWrapper)
{
if (mDestroyed) {
return;
}
MOZ_ASSERT(aWrTextureWrapper);
PipelineTexturesHolder* holder = mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
MOZ_ASSERT(holder);
if (!holder) {
return;
}
// Hold WebRenderTextureHostWrapper until end of its usage on RenderThread
holder->mTextureHostWrappers.push(ForwardingTextureHostWrapper(aEpoch, aWrTextureWrapper));
}
void
AsyncImagePipelineManager::HoldExternalImage(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch, const wr::ExternalImageId& aImageId)
{
if (mDestroyed) {
SharedSurfacesParent::Release(aImageId);
return;
}
PipelineTexturesHolder* holder = mPipelineTexturesHolders.Get(wr::AsUint64(aPipelineId));
MOZ_ASSERT(holder);
if (!holder) {
SharedSurfacesParent::Release(aImageId);
return;
}
holder->mExternalImages.push(ForwardingExternalImage(aEpoch, aImageId));
}
void
AsyncImagePipelineManager::NotifyPipelinesUpdated(wr::WrPipelineInfo aInfo)
{
// This is called on the render thread, so we just stash the data into
// mUpdatesQueue and process it later on the compositor thread.
MOZ_ASSERT(wr::RenderThread::IsInRenderThread());
MutexAutoLock lock(mUpdatesLock);
for (uintptr_t i = 0; i < aInfo.epochs.length; i++) {
mUpdatesQueue.push(std::make_pair(
aInfo.epochs.data[i].pipeline_id,
Some(aInfo.epochs.data[i].epoch)));
}
for (uintptr_t i = 0; i < aInfo.removed_pipelines.length; i++) {
mUpdatesQueue.push(std::make_pair(
aInfo.removed_pipelines.data[i],
Nothing()));
}
// Queue a runnable on the compositor thread to process the queue
layers::CompositorThreadHolder::Loop()->PostTask(
NewRunnableMethod("ProcessPipelineUpdates",
this,
&AsyncImagePipelineManager::ProcessPipelineUpdates));
}
void
AsyncImagePipelineManager::ProcessPipelineUpdates()
{
MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
if (mDestroyed) {
return;
}
while (true) {
wr::PipelineId pipelineId;
Maybe<wr::Epoch> epoch;
{ // scope lock to extract one item from the queue
MutexAutoLock lock(mUpdatesLock);
if (mUpdatesQueue.empty()) {
break;
}
pipelineId = mUpdatesQueue.front().first;
epoch = mUpdatesQueue.front().second;
mUpdatesQueue.pop();
}
if (epoch.isSome()) {
ProcessPipelineRendered(pipelineId, *epoch);
} else {
ProcessPipelineRemoved(pipelineId);
}
}
}
void
AsyncImagePipelineManager::ProcessPipelineRendered(const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch)
{
if (auto entry = mPipelineTexturesHolders.Lookup(wr::AsUint64(aPipelineId))) {
PipelineTexturesHolder* holder = entry.Data();
// Release TextureHosts based on Epoch
while (!holder->mTextureHosts.empty()) {
if (aEpoch <= holder->mTextureHosts.front().mEpoch) {
break;
}
holder->mTextureHosts.pop();
}
while (!holder->mTextureHostWrappers.empty()) {
if (aEpoch <= holder->mTextureHostWrappers.front().mEpoch) {
break;
}
holder->mTextureHostWrappers.pop();
}
while (!holder->mExternalImages.empty()) {
if (aEpoch <= holder->mExternalImages.front().mEpoch) {
break;
}
DebugOnly<bool> released =
SharedSurfacesParent::Release(holder->mExternalImages.front().mImageId);
MOZ_ASSERT(released);
holder->mExternalImages.pop();
}
}
}
void
AsyncImagePipelineManager::ProcessPipelineRemoved(const wr::PipelineId& aPipelineId)
{
if (mDestroyed) {
return;
}
if (auto entry = mPipelineTexturesHolders.Lookup(wr::AsUint64(aPipelineId))) {
PipelineTexturesHolder* holder = entry.Data();
if (holder->mDestroyedEpoch.isSome()) {
// Explicitly release all of the shared surfaces.
while (!holder->mExternalImages.empty()) {
DebugOnly<bool> released =
SharedSurfacesParent::Release(holder->mExternalImages.front().mImageId);
MOZ_ASSERT(released);
holder->mExternalImages.pop();
}
// Remove Pipeline
entry.Remove();
}
// If mDestroyedEpoch contains nothing it means we reused the same pipeline id (probably because
// we moved the tab to another window). In this case we need to keep the holder.
}
}
wr::Epoch
AsyncImagePipelineManager::GetNextImageEpoch()
{
mAsyncImageEpoch.mHandle++;
return mAsyncImageEpoch;
}
} // namespace layers
} // namespace mozilla