/* -*- 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& aRect, float aScale, nscolor aBackgroundColor, CrossProcessPaintFlags aFlags) { nsIDocShell* ds = aBc->GetDocShell(); if (!ds) { PF_LOG("Couldn't find docshell.\n"); return PaintFragment{}; } RefPtr presContext = ds->GetPresContext(); if (!presContext) { PF_LOG("Couldn't find PresContext.\n"); return PaintFragment{}; } CSSIntRect rect; if (!aRect) { nsCOMPtr 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 referenceDt = Factory::CreateDrawTarget( gfxPlatform::GetPlatform()->GetSoftwareBackend(), IntSize(1, 1), format); // TODO: This may OOM crash if the content is complex enough RefPtr recorder = MakeAndAddRef(nullptr); RefPtr 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 = presContext->PresShell(); Unused << presShell->RenderDocument(r, renderDocFlags, aBackgroundColor, &thebes); } if (!recorder->mOutputStream.mValid) { recorder->DetachResources(); 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; PaintFragment fragment{ surfaceSize.ToUnknownSize(), std::move(recording), std::move(recorder->TakeDependentSurfaces()), }; recorder->DetachResources(); return fragment; } bool PaintFragment::IsEmpty() const { return !mRecording.mData || mRecording.mLen == 0 || mSize == IntSize(0, 0); } PaintFragment::PaintFragment(IntSize aSize, ByteBuf&& aRecording, nsTHashSet&& 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 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 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 resolver = new CrossProcessPaint(aScale, rootId, aFlags); RefPtr promise; if (aRoot->IsInProcess()) { RefPtr childActor = aRoot->GetChildActor(); if (!childActor) { return false; } // `BrowsingContext()` cannot be nullptr. RefPtr 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 root = aFragments.Get(rootId); CPP_LOG("Resolved all fragments.\n"); // Create the destination draw target RefPtr 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 snapshot = drawTarget->Snapshot(); if (!snapshot) { promise->MaybeReject(NS_ERROR_FAILURE); return; } ErrorResult rv; RefPtr 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::Start( nsTHashSet&& aDependencies) { MOZ_ASSERT(!aDependencies.IsEmpty()); RefPtr resolver = new CrossProcessPaint(1.0, dom::TabId(0), CrossProcessPaintFlags::None); RefPtr 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() { Clear(NS_ERROR_ABORT); } 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& 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 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& 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 clonePromise = aBc->GetClonePromise(); if (!clonePromise) { RefPtr 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 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 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 surface = new RecordedDependentSurface{ fragment->mSize, std::move(fragment->mRecording)}; aResolved->InsertOrUpdate(aTabId, std::move(surface)); return NS_OK; } } // namespace gfx } // namespace mozilla