зеркало из https://github.com/mozilla/gecko-dev.git
526 строки
18 KiB
C++
526 строки
18 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 "CrossProcessPaint.h"
|
|
|
|
#include "mozilla/dom/CanonicalBrowsingContext.h"
|
|
#include "mozilla/dom/ContentProcessManager.h"
|
|
#include "mozilla/dom/ImageBitmap.h"
|
|
#include "mozilla/dom/BrowserParent.h"
|
|
#include "mozilla/dom/PWindowGlobalParent.h"
|
|
#include "mozilla/dom/Promise.h"
|
|
#include "mozilla/dom/WindowGlobalParent.h"
|
|
#include "mozilla/dom/WindowGlobalChild.h"
|
|
#include "mozilla/dom/WindowGlobalActorsBinding.h"
|
|
#include "mozilla/gfx/DrawEventRecorder.h"
|
|
#include "mozilla/gfx/InlineTranslator.h"
|
|
#include "mozilla/Logging.h"
|
|
#include "mozilla/PresShell.h"
|
|
|
|
#include "gfxPlatform.h"
|
|
|
|
#include "nsContentUtils.h"
|
|
#include "nsIDocShell.h"
|
|
#include "nsPresContext.h"
|
|
|
|
static mozilla::LazyLogModule gCrossProcessPaintLog("CrossProcessPaint");
|
|
static mozilla::LazyLogModule gPaintFragmentLog("PaintFragment");
|
|
|
|
#define CPP_LOG(msg, ...) \
|
|
MOZ_LOG(gCrossProcessPaintLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
|
|
#define PF_LOG(msg, ...) \
|
|
MOZ_LOG(gPaintFragmentLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
|
|
|
|
namespace mozilla {
|
|
namespace gfx {
|
|
|
|
using namespace mozilla::ipc;
|
|
|
|
/// The minimum scale we allow tabs to be rasterized at.
|
|
static const float kMinPaintScale = 0.05f;
|
|
|
|
/* static */
|
|
PaintFragment PaintFragment::Record(dom::BrowsingContext* aBc,
|
|
const Maybe<IntRect>& aRect, float aScale,
|
|
nscolor aBackgroundColor,
|
|
CrossProcessPaintFlags aFlags) {
|
|
nsIDocShell* ds = aBc->GetDocShell();
|
|
if (!ds) {
|
|
PF_LOG("Couldn't find docshell.\n");
|
|
return PaintFragment{};
|
|
}
|
|
|
|
RefPtr<nsPresContext> presContext = ds->GetPresContext();
|
|
if (!presContext) {
|
|
PF_LOG("Couldn't find PresContext.\n");
|
|
return PaintFragment{};
|
|
}
|
|
|
|
CSSIntRect rect;
|
|
if (!aRect) {
|
|
nsCOMPtr<nsIWidget> widget =
|
|
nsContentUtils::WidgetForDocument(presContext->Document());
|
|
|
|
// TODO: Apply some sort of clipping to visible bounds here (Bug 1562720)
|
|
LayoutDeviceIntRect boundsDevice = widget->GetBounds();
|
|
boundsDevice.MoveTo(0, 0);
|
|
nsRect boundsAu = LayoutDevicePixel::ToAppUnits(
|
|
boundsDevice, presContext->AppUnitsPerDevPixel());
|
|
rect = gfx::RoundedOut(CSSPixel::FromAppUnits(boundsAu));
|
|
} else {
|
|
rect = CSSIntRect::FromUnknownRect(*aRect);
|
|
}
|
|
|
|
if (rect.IsEmpty()) {
|
|
// TODO: Should we return an empty surface here?
|
|
PF_LOG("Empty rect to paint.\n");
|
|
return PaintFragment{};
|
|
}
|
|
|
|
// FIXME: Shouldn't the surface size be in device rather than CSS pixels?
|
|
CSSIntSize surfaceSize = rect.Size();
|
|
surfaceSize.width *= aScale;
|
|
surfaceSize.height *= aScale;
|
|
|
|
CPP_LOG(
|
|
"Recording "
|
|
"[browsingContext=%p, "
|
|
"rect=(%d, %d) x (%d, %d), "
|
|
"scale=%f, "
|
|
"color=(%u, %u, %u, %u)]\n",
|
|
aBc, rect.x, rect.y, rect.width, rect.height, aScale,
|
|
NS_GET_R(aBackgroundColor), NS_GET_G(aBackgroundColor),
|
|
NS_GET_B(aBackgroundColor), NS_GET_A(aBackgroundColor));
|
|
|
|
// Check for invalid sizes
|
|
if (surfaceSize.width <= 0 || surfaceSize.height <= 0 ||
|
|
!Factory::CheckSurfaceSize(surfaceSize.ToUnknownSize())) {
|
|
PF_LOG("Invalid surface size of (%d x %d).\n", surfaceSize.width,
|
|
surfaceSize.height);
|
|
return PaintFragment{};
|
|
}
|
|
|
|
// Flush any pending notifications
|
|
nsContentUtils::FlushLayoutForTree(ds->GetWindow());
|
|
|
|
// Initialize the recorder
|
|
SurfaceFormat format = SurfaceFormat::B8G8R8A8;
|
|
RefPtr<DrawTarget> referenceDt = Factory::CreateDrawTarget(
|
|
gfxPlatform::GetPlatform()->GetSoftwareBackend(), IntSize(1, 1), format);
|
|
|
|
// TODO: This may OOM crash if the content is complex enough
|
|
RefPtr<DrawEventRecorderMemory> recorder =
|
|
MakeAndAddRef<DrawEventRecorderMemory>(nullptr);
|
|
RefPtr<DrawTarget> dt = Factory::CreateRecordingDrawTarget(
|
|
recorder, referenceDt,
|
|
IntRect(IntPoint(0, 0), surfaceSize.ToUnknownSize()));
|
|
if (!dt || !dt->IsValid()) {
|
|
PF_LOG("Failed to create drawTarget.\n");
|
|
return PaintFragment{};
|
|
}
|
|
|
|
RenderDocumentFlags renderDocFlags = RenderDocumentFlags::None;
|
|
if (!(aFlags & CrossProcessPaintFlags::DrawView)) {
|
|
renderDocFlags |= RenderDocumentFlags::IgnoreViewportScrolling |
|
|
RenderDocumentFlags::DocumentRelative;
|
|
if (aFlags & CrossProcessPaintFlags::ResetScrollPosition) {
|
|
renderDocFlags |= RenderDocumentFlags::ResetViewportScrolling;
|
|
}
|
|
}
|
|
if (aFlags & CrossProcessPaintFlags::UseHighQualityScaling) {
|
|
renderDocFlags |= RenderDocumentFlags::UseHighQualityScaling;
|
|
}
|
|
|
|
// Perform the actual rendering
|
|
{
|
|
nsRect r = CSSPixel::ToAppUnits(rect);
|
|
|
|
// This matches what nsDeviceContext::CreateRenderingContext does.
|
|
if (presContext->IsPrintingOrPrintPreview()) {
|
|
dt->AddUserData(&sDisablePixelSnapping, (void*)0x1, nullptr);
|
|
}
|
|
|
|
gfxContext thebes(dt);
|
|
thebes.SetMatrix(Matrix::Scaling(aScale, aScale));
|
|
thebes.SetCrossProcessPaintScale(aScale);
|
|
RefPtr<PresShell> presShell = presContext->PresShell();
|
|
Unused << presShell->RenderDocument(r, renderDocFlags, aBackgroundColor,
|
|
&thebes);
|
|
}
|
|
|
|
if (!recorder->mOutputStream.mValid) {
|
|
return PaintFragment{};
|
|
}
|
|
|
|
ByteBuf recording = ByteBuf((uint8_t*)recorder->mOutputStream.mData,
|
|
recorder->mOutputStream.mLength,
|
|
recorder->mOutputStream.mCapacity);
|
|
recorder->mOutputStream.mData = nullptr;
|
|
recorder->mOutputStream.mLength = 0;
|
|
recorder->mOutputStream.mCapacity = 0;
|
|
|
|
return PaintFragment{
|
|
surfaceSize.ToUnknownSize(),
|
|
std::move(recording),
|
|
std::move(recorder->TakeDependentSurfaces()),
|
|
};
|
|
}
|
|
|
|
bool PaintFragment::IsEmpty() const {
|
|
return !mRecording.mData || mRecording.mLen == 0 || mSize == IntSize(0, 0);
|
|
}
|
|
|
|
PaintFragment::PaintFragment(IntSize aSize, ByteBuf&& aRecording,
|
|
nsTHashSet<uint64_t>&& aDependencies)
|
|
: mSize(aSize),
|
|
mRecording(std::move(aRecording)),
|
|
mDependencies(std::move(aDependencies)) {}
|
|
|
|
static dom::TabId GetTabId(dom::WindowGlobalParent* aWGP) {
|
|
// There is no unique TabId for a given WindowGlobalParent, as multiple
|
|
// WindowGlobalParents share the same PBrowser actor. However, we only
|
|
// ever queue one paint per PBrowser by just using the current
|
|
// WindowGlobalParent for a PBrowser. So we can interchange TabId and
|
|
// WindowGlobalParent when dealing with resolving surfaces.
|
|
RefPtr<dom::BrowserParent> browserParent = aWGP->GetBrowserParent();
|
|
return browserParent ? browserParent->GetTabId() : dom::TabId(0);
|
|
}
|
|
|
|
/* static */
|
|
bool CrossProcessPaint::Start(dom::WindowGlobalParent* aRoot,
|
|
const dom::DOMRect* aRect, float aScale,
|
|
nscolor aBackgroundColor,
|
|
CrossProcessPaintFlags aFlags,
|
|
dom::Promise* aPromise) {
|
|
MOZ_RELEASE_ASSERT(XRE_IsParentProcess());
|
|
aScale = std::max(aScale, kMinPaintScale);
|
|
|
|
CPP_LOG(
|
|
"Starting paint. "
|
|
"[wgp=%p, "
|
|
"scale=%f, "
|
|
"color=(%u, %u, %u, %u)]\n",
|
|
aRoot, aScale, NS_GET_R(aBackgroundColor), NS_GET_G(aBackgroundColor),
|
|
NS_GET_B(aBackgroundColor), NS_GET_A(aBackgroundColor));
|
|
|
|
Maybe<IntRect> rect;
|
|
if (aRect) {
|
|
rect =
|
|
Some(IntRect::RoundOut((float)aRect->X(), (float)aRect->Y(),
|
|
(float)aRect->Width(), (float)aRect->Height()));
|
|
}
|
|
|
|
if (rect && rect->IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
dom::TabId rootId = GetTabId(aRoot);
|
|
|
|
RefPtr<CrossProcessPaint> resolver =
|
|
new CrossProcessPaint(aScale, rootId, aFlags);
|
|
RefPtr<CrossProcessPaint::ResolvePromise> promise;
|
|
if (aRoot->IsInProcess()) {
|
|
RefPtr<dom::WindowGlobalChild> childActor = aRoot->GetChildActor();
|
|
if (!childActor) {
|
|
return false;
|
|
}
|
|
|
|
// `BrowsingContext()` cannot be nullptr.
|
|
RefPtr<dom::BrowsingContext> bc = childActor->BrowsingContext();
|
|
|
|
promise = resolver->Init();
|
|
resolver->mPendingFragments += 1;
|
|
resolver->ReceiveFragment(
|
|
aRoot,
|
|
PaintFragment::Record(bc, rect, aScale, aBackgroundColor, aFlags));
|
|
} else {
|
|
promise = resolver->Init();
|
|
resolver->QueuePaint(aRoot, rect, aBackgroundColor, aFlags);
|
|
}
|
|
|
|
promise->Then(
|
|
GetMainThreadSerialEventTarget(), __func__,
|
|
[promise = RefPtr{aPromise}, rootId](ResolvedFragmentMap&& aFragments) {
|
|
RefPtr<RecordedDependentSurface> root = aFragments.Get(rootId);
|
|
CPP_LOG("Resolved all fragments.\n");
|
|
|
|
// Create the destination draw target
|
|
RefPtr<DrawTarget> drawTarget =
|
|
gfxPlatform::GetPlatform()->CreateOffscreenContentDrawTarget(
|
|
root->mSize, SurfaceFormat::B8G8R8A8);
|
|
if (!drawTarget || !drawTarget->IsValid()) {
|
|
CPP_LOG("Couldn't create (%d x %d) surface for fragment %" PRIu64
|
|
".\n",
|
|
root->mSize.width, root->mSize.height, (uint64_t)rootId);
|
|
promise->MaybeReject(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
// Translate the recording using our child tabs
|
|
{
|
|
InlineTranslator translator(drawTarget, nullptr);
|
|
translator.SetDependentSurfaces(&aFragments);
|
|
if (!translator.TranslateRecording((char*)root->mRecording.mData,
|
|
root->mRecording.mLen)) {
|
|
CPP_LOG("Couldn't translate recording for fragment %" PRIu64 ".\n",
|
|
(uint64_t)rootId);
|
|
promise->MaybeReject(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
RefPtr<SourceSurface> snapshot = drawTarget->Snapshot();
|
|
if (!snapshot) {
|
|
promise->MaybeReject(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
ErrorResult rv;
|
|
RefPtr<dom::ImageBitmap> bitmap =
|
|
dom::ImageBitmap::CreateFromSourceSurface(
|
|
promise->GetParentObject(), snapshot, rv);
|
|
|
|
if (!rv.Failed()) {
|
|
CPP_LOG("Success, fulfilling promise.\n");
|
|
promise->MaybeResolve(bitmap);
|
|
} else {
|
|
CPP_LOG("Couldn't create ImageBitmap for SourceSurface.\n");
|
|
promise->MaybeReject(std::move(rv));
|
|
}
|
|
},
|
|
[promise = RefPtr{aPromise}](const nsresult& aRv) {
|
|
promise->MaybeReject(aRv);
|
|
});
|
|
|
|
return true;
|
|
}
|
|
|
|
/* static */
|
|
RefPtr<CrossProcessPaint::ResolvePromise> CrossProcessPaint::Start(
|
|
nsTHashSet<uint64_t>&& aDependencies) {
|
|
MOZ_ASSERT(!aDependencies.IsEmpty());
|
|
RefPtr<CrossProcessPaint> resolver =
|
|
new CrossProcessPaint(1.0, dom::TabId(0), CrossProcessPaintFlags::None);
|
|
|
|
RefPtr<CrossProcessPaint::ResolvePromise> promise = resolver->Init();
|
|
|
|
PaintFragment rootFragment;
|
|
rootFragment.mDependencies = std::move(aDependencies);
|
|
|
|
resolver->QueueDependencies(rootFragment.mDependencies);
|
|
resolver->mReceivedFragments.InsertOrUpdate(dom::TabId(0),
|
|
std::move(rootFragment));
|
|
|
|
resolver->MaybeResolve();
|
|
|
|
return promise;
|
|
}
|
|
|
|
CrossProcessPaint::CrossProcessPaint(float aScale, dom::TabId aRoot,
|
|
CrossProcessPaintFlags aFlags)
|
|
: mRoot{aRoot}, mScale{aScale}, mPendingFragments{0}, mFlags{aFlags} {}
|
|
|
|
CrossProcessPaint::~CrossProcessPaint() = default;
|
|
|
|
void CrossProcessPaint::ReceiveFragment(dom::WindowGlobalParent* aWGP,
|
|
PaintFragment&& aFragment) {
|
|
if (IsCleared()) {
|
|
CPP_LOG("Ignoring fragment from %p.\n", aWGP);
|
|
return;
|
|
}
|
|
|
|
dom::TabId surfaceId = GetTabId(aWGP);
|
|
|
|
MOZ_ASSERT(mPendingFragments > 0);
|
|
MOZ_ASSERT(!mReceivedFragments.Contains(surfaceId));
|
|
|
|
// Double check our invariants to protect against a compromised content
|
|
// process
|
|
if (mPendingFragments == 0 || mReceivedFragments.Contains(surfaceId) ||
|
|
aFragment.IsEmpty()) {
|
|
CPP_LOG("Dropping invalid fragment from %p.\n", aWGP);
|
|
LostFragment(aWGP);
|
|
return;
|
|
}
|
|
|
|
CPP_LOG("Receiving fragment from %p(%" PRIu64 ").\n", aWGP,
|
|
(uint64_t)surfaceId);
|
|
|
|
// Queue paints for child tabs
|
|
QueueDependencies(aFragment.mDependencies);
|
|
|
|
mReceivedFragments.InsertOrUpdate(surfaceId, std::move(aFragment));
|
|
mPendingFragments -= 1;
|
|
|
|
// Resolve this paint if we have received all pending fragments
|
|
MaybeResolve();
|
|
}
|
|
|
|
void CrossProcessPaint::LostFragment(dom::WindowGlobalParent* aWGP) {
|
|
if (IsCleared()) {
|
|
CPP_LOG("Ignoring lost fragment from %p.\n", aWGP);
|
|
return;
|
|
}
|
|
|
|
Clear(NS_ERROR_LOSS_OF_SIGNIFICANT_DATA);
|
|
}
|
|
|
|
void CrossProcessPaint::QueueDependencies(
|
|
const nsTHashSet<uint64_t>& aDependencies) {
|
|
dom::ContentProcessManager* cpm = dom::ContentProcessManager::GetSingleton();
|
|
if (!cpm) {
|
|
CPP_LOG(
|
|
"Skipping QueueDependencies with no"
|
|
" current ContentProcessManager.\n");
|
|
return;
|
|
}
|
|
for (const auto& key : aDependencies) {
|
|
auto dependency = dom::TabId(key);
|
|
|
|
// Get the current BrowserParent of the remote browser that was marked
|
|
// as a dependency
|
|
dom::ContentParentId cpId = cpm->GetTabProcessId(dependency);
|
|
RefPtr<dom::BrowserParent> browser =
|
|
cpm->GetBrowserParentByProcessAndTabId(cpId, dependency);
|
|
if (!browser) {
|
|
CPP_LOG("Skipping dependency %" PRIu64
|
|
" with no current BrowserParent.\n",
|
|
(uint64_t)dependency);
|
|
continue;
|
|
}
|
|
|
|
// Note that if the remote document is currently being cloned, it's possible
|
|
// that the BrowserParent isn't the one for the cloned document, but the
|
|
// BrowsingContext should be persisted/consistent.
|
|
QueuePaint(browser->GetBrowsingContext());
|
|
}
|
|
}
|
|
|
|
void CrossProcessPaint::QueuePaint(dom::WindowGlobalParent* aWGP,
|
|
const Maybe<IntRect>& aRect,
|
|
nscolor aBackgroundColor,
|
|
CrossProcessPaintFlags aFlags) {
|
|
MOZ_ASSERT(!mReceivedFragments.Contains(GetTabId(aWGP)));
|
|
|
|
CPP_LOG("Queueing paint for WindowGlobalParent(%p).\n", aWGP);
|
|
|
|
aWGP->DrawSnapshotInternal(this, aRect, mScale, aBackgroundColor,
|
|
(uint32_t)aFlags);
|
|
mPendingFragments += 1;
|
|
}
|
|
|
|
void CrossProcessPaint::QueuePaint(dom::CanonicalBrowsingContext* aBc) {
|
|
RefPtr<GenericNonExclusivePromise> clonePromise = aBc->GetClonePromise();
|
|
|
|
if (!clonePromise) {
|
|
RefPtr<dom::WindowGlobalParent> wgp = aBc->GetCurrentWindowGlobal();
|
|
if (!wgp) {
|
|
CPP_LOG("Skipping BrowsingContext(%p) with no current WGP.\n", aBc);
|
|
return;
|
|
}
|
|
|
|
// TODO: Apply some sort of clipping to visible bounds here (Bug 1562720)
|
|
QueuePaint(wgp, Nothing(), NS_RGBA(0, 0, 0, 0), GetFlagsForDependencies());
|
|
return;
|
|
}
|
|
|
|
CPP_LOG("Queueing paint for BrowsingContext(%p).\n", aBc);
|
|
// In the case it's still in the process of cloning the remote document, we
|
|
// should defer the snapshot request after the cloning has been finished.
|
|
mPendingFragments += 1;
|
|
clonePromise->Then(
|
|
GetMainThreadSerialEventTarget(), __func__,
|
|
[self = RefPtr{this}, bc = RefPtr{aBc}]() {
|
|
RefPtr<dom::WindowGlobalParent> wgp = bc->GetCurrentWindowGlobal();
|
|
if (!wgp) {
|
|
CPP_LOG("Skipping BrowsingContext(%p) with no current WGP.\n",
|
|
bc.get());
|
|
return;
|
|
}
|
|
MOZ_ASSERT(!self->mReceivedFragments.Contains(GetTabId(wgp)));
|
|
|
|
// TODO: Apply some sort of clipping to visible bounds here (Bug
|
|
// 1562720)
|
|
wgp->DrawSnapshotInternal(self, Nothing(), self->mScale,
|
|
NS_RGBA(0, 0, 0, 0),
|
|
(uint32_t)self->GetFlagsForDependencies());
|
|
},
|
|
[self = RefPtr{this}]() {
|
|
CPP_LOG(
|
|
"Abort painting for BrowsingContext(%p) because cloning remote "
|
|
"document failed.\n",
|
|
self.get());
|
|
self->Clear(NS_ERROR_FAILURE);
|
|
});
|
|
}
|
|
|
|
void CrossProcessPaint::Clear(nsresult aStatus) {
|
|
mPendingFragments = 0;
|
|
mReceivedFragments.Clear();
|
|
mPromise.RejectIfExists(aStatus, __func__);
|
|
}
|
|
|
|
bool CrossProcessPaint::IsCleared() const { return mPromise.IsEmpty(); }
|
|
|
|
void CrossProcessPaint::MaybeResolve() {
|
|
// Don't do anything if we aren't ready, experienced an error, or already
|
|
// resolved this paint
|
|
if (IsCleared() || mPendingFragments > 0) {
|
|
CPP_LOG("Not ready to resolve yet, have %u fragments left.\n",
|
|
mPendingFragments);
|
|
return;
|
|
}
|
|
|
|
CPP_LOG("Starting to resolve fragments.\n");
|
|
|
|
// Resolve the paint fragments from the bottom up
|
|
ResolvedFragmentMap resolved;
|
|
{
|
|
nsresult rv = ResolveInternal(mRoot, &resolved);
|
|
if (NS_FAILED(rv)) {
|
|
CPP_LOG("Couldn't resolve.\n");
|
|
Clear(rv);
|
|
return;
|
|
}
|
|
}
|
|
|
|
CPP_LOG("Resolved all fragments.\n");
|
|
|
|
mPromise.ResolveIfExists(std::move(resolved), __func__);
|
|
Clear(NS_OK);
|
|
}
|
|
|
|
nsresult CrossProcessPaint::ResolveInternal(dom::TabId aTabId,
|
|
ResolvedFragmentMap* aResolved) {
|
|
// We should not have resolved this paint already
|
|
MOZ_ASSERT(!aResolved->GetWeak(aTabId));
|
|
|
|
CPP_LOG("Resolving fragment %" PRIu64 ".\n", (uint64_t)aTabId);
|
|
|
|
Maybe<PaintFragment> fragment = mReceivedFragments.Extract(aTabId);
|
|
if (!fragment) {
|
|
return NS_ERROR_LOSS_OF_SIGNIFICANT_DATA;
|
|
}
|
|
|
|
// Rasterize all the dependencies first so that we can resolve this fragment
|
|
for (const auto& key : fragment->mDependencies) {
|
|
auto dependency = dom::TabId(key);
|
|
|
|
nsresult rv = ResolveInternal(dependency, aResolved);
|
|
if (NS_FAILED(rv)) {
|
|
return rv;
|
|
}
|
|
}
|
|
|
|
RefPtr<RecordedDependentSurface> surface = new RecordedDependentSurface{
|
|
fragment->mSize, std::move(fragment->mRecording)};
|
|
aResolved->InsertOrUpdate(aTabId, std::move(surface));
|
|
return NS_OK;
|
|
}
|
|
|
|
} // namespace gfx
|
|
} // namespace mozilla
|