/* -*- 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 "CanvasChild.h" #include "MainThreadUtils.h" #include "mozilla/dom/WorkerPrivate.h" #include "mozilla/dom/WorkerRef.h" #include "mozilla/dom/WorkerRunnable.h" #include "mozilla/gfx/CanvasManagerChild.h" #include "mozilla/gfx/DrawTargetRecording.h" #include "mozilla/gfx/Tools.h" #include "mozilla/gfx/Rect.h" #include "mozilla/gfx/Point.h" #include "mozilla/ipc/Endpoint.h" #include "mozilla/ipc/ProcessChild.h" #include "mozilla/layers/CanvasDrawEventRecorder.h" #include "mozilla/layers/ImageDataSerializer.h" #include "mozilla/layers/SourceSurfaceSharedData.h" #include "mozilla/Maybe.h" #include "mozilla/Mutex.h" #include "nsIObserverService.h" #include "RecordedCanvasEventImpl.h" namespace mozilla { namespace layers { class RecorderHelpers final : public CanvasDrawEventRecorder::Helpers { public: NS_DECL_OWNINGTHREAD explicit RecorderHelpers(const RefPtr& aCanvasChild) : mCanvasChild(aCanvasChild) {} ~RecorderHelpers() override = default; bool InitTranslator(TextureType aTextureType, TextureType aWebglTextureType, gfx::BackendType aBackendType, Handle&& aReadHandle, nsTArray&& aBufferHandles, uint64_t aBufferSize, CrossProcessSemaphoreHandle&& aReaderSem, CrossProcessSemaphoreHandle&& aWriterSem) override { NS_ASSERT_OWNINGTHREAD(RecorderHelpers); if (NS_WARN_IF(!mCanvasChild)) { return false; } return mCanvasChild->SendInitTranslator( aTextureType, aWebglTextureType, aBackendType, std::move(aReadHandle), std::move(aBufferHandles), aBufferSize, std::move(aReaderSem), std::move(aWriterSem)); } bool AddBuffer(Handle&& aBufferHandle, uint64_t aBufferSize) override { NS_ASSERT_OWNINGTHREAD(RecorderHelpers); if (!mCanvasChild) { return false; } return mCanvasChild->SendAddBuffer(std::move(aBufferHandle), aBufferSize); } bool ReaderClosed() override { NS_ASSERT_OWNINGTHREAD(RecorderHelpers); if (!mCanvasChild) { return false; } return !mCanvasChild->CanSend() || ipc::ProcessChild::ExpectingShutdown(); } bool RestartReader() override { NS_ASSERT_OWNINGTHREAD(RecorderHelpers); if (!mCanvasChild) { return false; } return mCanvasChild->SendRestartTranslation(); } private: const WeakPtr mCanvasChild; }; class SourceSurfaceCanvasRecording final : public gfx::SourceSurface { public: MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceCanvasRecording, final) SourceSurfaceCanvasRecording( int64_t aTextureId, const RefPtr& aRecordedSuface, CanvasChild* aCanvasChild, const RefPtr& aRecorder) : mTextureId(aTextureId), mRecordedSurface(aRecordedSuface), mCanvasChild(aCanvasChild), mRecorder(aRecorder) { // It's important that AddStoredObject is called first because that will // run any pending processing required by recorded objects that have been // deleted off the main thread. mRecorder->AddStoredObject(this); mRecorder->RecordEvent(RecordedAddSurfaceAlias(this, aRecordedSuface)); } ~SourceSurfaceCanvasRecording() { ReferencePtr surfaceAlias = this; if (NS_IsMainThread()) { ReleaseOnMainThread(std::move(mRecorder), surfaceAlias, std::move(mRecordedSurface), std::move(mCanvasChild)); return; } mRecorder->AddPendingDeletion( [recorder = std::move(mRecorder), surfaceAlias, aliasedSurface = std::move(mRecordedSurface), canvasChild = std::move(mCanvasChild)]() mutable -> void { ReleaseOnMainThread(std::move(recorder), surfaceAlias, std::move(aliasedSurface), std::move(canvasChild)); }); } gfx::SurfaceType GetType() const final { return mRecordedSurface->GetType(); } gfx::IntSize GetSize() const final { return mRecordedSurface->GetSize(); } gfx::SurfaceFormat GetFormat() const final { return mRecordedSurface->GetFormat(); } already_AddRefed GetDataSurface() final { EnsureDataSurfaceOnMainThread(); return do_AddRef(mDataSourceSurface); } void AttachSurface() { mDetached = false; } void DetachSurface() { mDetached = true; } already_AddRefed ExtractSubrect( const gfx::IntRect& aRect) final { return mRecordedSurface->ExtractSubrect(aRect); } private: void EnsureDataSurfaceOnMainThread() { // The data can only be retrieved on the main thread. if (!mDataSourceSurface && NS_IsMainThread()) { mDataSourceSurface = mCanvasChild->GetDataSurface(mTextureId, mRecordedSurface, mDetached); } } // Used to ensure that clean-up that requires it is done on the main thread. static void ReleaseOnMainThread(RefPtr aRecorder, ReferencePtr aSurfaceAlias, RefPtr aAliasedSurface, RefPtr aCanvasChild) { MOZ_ASSERT(NS_IsMainThread()); aRecorder->RemoveStoredObject(aSurfaceAlias); aRecorder->RecordEvent(RecordedRemoveSurfaceAlias(aSurfaceAlias)); aAliasedSurface = nullptr; aCanvasChild = nullptr; aRecorder = nullptr; } int64_t mTextureId; RefPtr mRecordedSurface; RefPtr mCanvasChild; RefPtr mRecorder; RefPtr mDataSourceSurface; bool mDetached = false; }; class CanvasDataShmemHolder { public: CanvasDataShmemHolder(ipc::SharedMemoryBasic* aShmem, CanvasChild* aCanvasChild) : mMutex("CanvasChild::DataShmemHolder::mMutex"), mShmem(aShmem), mCanvasChild(aCanvasChild) {} bool Init(dom::ThreadSafeWorkerRef* aWorkerRef) { if (!aWorkerRef) { return true; } RefPtr workerRef = dom::StrongWorkerRef::Create( aWorkerRef->Private(), "CanvasChild::DataShmemHolder", [this]() { DestroyWorker(); }); if (NS_WARN_IF(!workerRef)) { return false; } MutexAutoLock lock(mMutex); mWorkerRef = new dom::ThreadSafeWorkerRef(workerRef); return true; } void Destroy() { class DestroyRunnable final : public dom::WorkerRunnable { public: DestroyRunnable(dom::WorkerPrivate* aWorkerPrivate, CanvasDataShmemHolder* aShmemHolder) : dom::WorkerRunnable(aWorkerPrivate, "CanvasDataShmemHolder::Destroy", dom::WorkerRunnable::WorkerThread), mShmemHolder(aShmemHolder) {} bool WorkerRun(JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate) override { mShmemHolder->Destroy(); return true; } void PostRun(JSContext* aCx, dom::WorkerPrivate* aWorkerPrivate, bool aRunResult) override {} bool PreDispatch(dom::WorkerPrivate* aWorkerPrivate) override { return true; } void PostDispatch(dom::WorkerPrivate* aWorkerPrivate, bool aDispatchResult) override {} private: CanvasDataShmemHolder* mShmemHolder; }; mMutex.Lock(); if (mCanvasChild) { if (mWorkerRef) { if (!mWorkerRef->Private()->IsOnCurrentThread()) { auto task = MakeRefPtr(mWorkerRef->Private(), this); mMutex.Unlock(); task->Dispatch(); return; } } else if (!NS_IsMainThread()) { mMutex.Unlock(); NS_DispatchToMainThread(NS_NewRunnableFunction( "CanvasDataShmemHolder::Destroy", [this]() { Destroy(); })); return; } mCanvasChild->ReturnDataSurfaceShmem(mShmem.forget()); mCanvasChild = nullptr; mWorkerRef = nullptr; } mMutex.Unlock(); delete this; } void DestroyWorker() { MutexAutoLock lock(mMutex); mCanvasChild = nullptr; mWorkerRef = nullptr; } private: Mutex mMutex; RefPtr mShmem; RefPtr mCanvasChild MOZ_GUARDED_BY(mMutex); RefPtr mWorkerRef MOZ_GUARDED_BY(mMutex); }; CanvasChild::CanvasChild(dom::ThreadSafeWorkerRef* aWorkerRef) : mWorkerRef(aWorkerRef) {} CanvasChild::~CanvasChild() { MOZ_ASSERT(!mWorkerRef); } static void NotifyCanvasDeviceReset() { nsCOMPtr obs = services::GetObserverService(); if (obs) { obs->NotifyObservers(nullptr, "canvas-device-reset", nullptr); } } ipc::IPCResult CanvasChild::RecvNotifyDeviceChanged() { NS_ASSERT_OWNINGTHREAD(CanvasChild); NotifyCanvasDeviceReset(); mRecorder->RecordEvent(RecordedDeviceChangeAcknowledged()); return IPC_OK(); } /* static */ bool CanvasChild::mDeactivated = false; ipc::IPCResult CanvasChild::RecvDeactivate() { NS_ASSERT_OWNINGTHREAD(CanvasChild); RefPtr self(this); mDeactivated = true; if (auto* cm = gfx::CanvasManagerChild::Get()) { cm->DeactivateCanvas(); } NotifyCanvasDeviceReset(); return IPC_OK(); } ipc::IPCResult CanvasChild::RecvBlockCanvas() { mBlocked = true; if (auto* cm = gfx::CanvasManagerChild::Get()) { cm->BlockCanvas(); } return IPC_OK(); } void CanvasChild::EnsureRecorder(gfx::IntSize aSize, gfx::SurfaceFormat aFormat, TextureType aTextureType, TextureType aWebglTextureType) { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (!mRecorder) { gfx::BackendType backendType = gfxPlatform::GetPlatform()->GetPreferredCanvasBackend(); auto recorder = MakeRefPtr(mWorkerRef); if (!recorder->Init(aTextureType, aWebglTextureType, backendType, MakeUnique(this))) { return; } mRecorder = recorder.forget(); } MOZ_RELEASE_ASSERT(mRecorder->GetTextureType() == aTextureType, "We only support one remote TextureType currently."); EnsureDataSurfaceShmem(aSize, aFormat); } void CanvasChild::ActorDestroy(ActorDestroyReason aWhy) { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (mRecorder) { mRecorder->DetachResources(); } } void CanvasChild::Destroy() { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (CanSend()) { Send__delete__(this); } mWorkerRef = nullptr; } bool CanvasChild::EnsureBeginTransaction() { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (!mIsInTransaction) { RecordEvent(RecordedCanvasBeginTransaction()); mIsInTransaction = true; } return true; } void CanvasChild::EndTransaction() { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (mIsInTransaction) { RecordEvent(RecordedCanvasEndTransaction()); mIsInTransaction = false; mDormant = false; } else if (mRecorder) { // Schedule to drop free buffers if we have no non-empty transactions. if (!mDormant) { mDormant = true; NS_DelayedDispatchToCurrentThread( NewRunnableMethod("CanvasChild::DropFreeBuffersWhenDormant", this, &CanvasChild::DropFreeBuffersWhenDormant), StaticPrefs::gfx_canvas_remote_drop_buffer_milliseconds()); } } ++mTransactionsSinceGetDataSurface; } void CanvasChild::DropFreeBuffersWhenDormant() { NS_ASSERT_OWNINGTHREAD(CanvasChild); // Drop any free buffers if we have not had any non-empty transactions. if (mDormant && mRecorder) { mRecorder->DropFreeBuffers(); } } void CanvasChild::ClearCachedResources() { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (mRecorder) { mRecorder->DropFreeBuffers(); // Notify CanvasTranslator it is about to be minimized. SendClearCachedResources(); } } bool CanvasChild::ShouldBeCleanedUp() const { NS_ASSERT_OWNINGTHREAD(CanvasChild); // Always return true if we've been deactivated. if (Deactivated()) { return true; } // We can only be cleaned up if nothing else references our recorder. return !mRecorder || mRecorder->hasOneRef(); } already_AddRefed CanvasChild::CreateDrawTarget( int64_t aTextureId, const RemoteTextureOwnerId& aTextureOwnerId, gfx::IntSize aSize, gfx::SurfaceFormat aFormat) { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (!mRecorder) { return nullptr; } RefPtr dummyDt = gfx::Factory::CreateDrawTarget( gfx::BackendType::SKIA, gfx::IntSize(1, 1), aFormat); RefPtr dt = MakeAndAddRef( mRecorder, aTextureId, aTextureOwnerId, dummyDt, aSize); mTextureInfo.insert({aTextureId, {}}); return dt.forget(); } bool CanvasChild::EnsureDataSurfaceShmem(gfx::IntSize aSize, gfx::SurfaceFormat aFormat) { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (!mRecorder) { return false; } size_t sizeRequired = ImageDataSerializer::ComputeRGBBufferSize(aSize, aFormat); if (!sizeRequired) { return false; } sizeRequired = ipc::SharedMemory::PageAlignedSize(sizeRequired); if (!mDataSurfaceShmemAvailable || mDataSurfaceShmem->Size() < sizeRequired) { RecordEvent(RecordedPauseTranslation()); auto dataSurfaceShmem = MakeRefPtr(); if (!dataSurfaceShmem->Create(sizeRequired) || !dataSurfaceShmem->Map(sizeRequired)) { return false; } auto shmemHandle = dataSurfaceShmem->TakeHandle(); if (!shmemHandle) { return false; } if (!SendSetDataSurfaceBuffer(std::move(shmemHandle), sizeRequired)) { return false; } mDataSurfaceShmem = dataSurfaceShmem.forget(); mDataSurfaceShmemAvailable = true; } MOZ_ASSERT(mDataSurfaceShmemAvailable); return true; } void CanvasChild::RecordEvent(const gfx::RecordedEvent& aEvent) { NS_ASSERT_OWNINGTHREAD(CanvasChild); // We drop mRecorder in ActorDestroy to break the reference cycle. if (!mRecorder) { return; } mRecorder->RecordEvent(aEvent); } int64_t CanvasChild::CreateCheckpoint() { NS_ASSERT_OWNINGTHREAD(CanvasChild); return mRecorder->CreateCheckpoint(); } already_AddRefed CanvasChild::GetDataSurface( int64_t aTextureId, const gfx::SourceSurface* aSurface, bool aDetached) { NS_ASSERT_OWNINGTHREAD(CanvasChild); MOZ_ASSERT(aSurface); // mTransactionsSinceGetDataSurface is used to determine if we want to prepare // a DataSourceSurface in the GPU process up front at the end of the // transaction, but that only makes sense if the canvas JS is requesting data // in between transactions. if (!mIsInTransaction) { mTransactionsSinceGetDataSurface = 0; } if (!EnsureBeginTransaction()) { return nullptr; } gfx::IntSize ssSize = aSurface->GetSize(); gfx::SurfaceFormat ssFormat = aSurface->GetFormat(); auto stride = ImageDataSerializer::ComputeRGBStride(ssFormat, ssSize.width); // Shmem is only valid if the surface is the latest snapshot (not detached). if (!aDetached) { // If there is a shmem associated with this snapshot id, then we want to try // use that directly without having to allocate a new shmem for retrieval. auto it = mTextureInfo.find(aTextureId); if (it != mTextureInfo.end() && it->second.mSnapshotShmem) { const auto shmemPtr = reinterpret_cast(it->second.mSnapshotShmem->memory()); MOZ_ASSERT(shmemPtr); mRecorder->RecordEvent(RecordedPrepareShmem(aTextureId)); auto checkpoint = CreateCheckpoint(); if (NS_WARN_IF(!mRecorder->WaitForCheckpoint(checkpoint))) { return nullptr; } auto* closure = new CanvasDataShmemHolder(it->second.mSnapshotShmem, this); if (NS_WARN_IF(!closure->Init(mWorkerRef))) { delete closure; return nullptr; } RefPtr dataSurface = gfx::Factory::CreateWrappingDataSourceSurface( shmemPtr, stride, ssSize, ssFormat, ReleaseDataShmemHolder, closure); return dataSurface.forget(); } } RecordEvent(RecordedPrepareDataForSurface(aSurface)); if (!EnsureDataSurfaceShmem(ssSize, ssFormat)) { return nullptr; } RecordEvent(RecordedGetDataForSurface(aSurface)); auto checkpoint = CreateCheckpoint(); if (NS_WARN_IF(!mRecorder->WaitForCheckpoint(checkpoint))) { return nullptr; } auto* closure = new CanvasDataShmemHolder(mDataSurfaceShmem, this); if (NS_WARN_IF(!closure->Init(mWorkerRef))) { delete closure; return nullptr; } mDataSurfaceShmemAvailable = false; auto* data = static_cast(mDataSurfaceShmem->memory()); RefPtr dataSurface = gfx::Factory::CreateWrappingDataSourceSurface( data, stride, ssSize, ssFormat, ReleaseDataShmemHolder, closure); return dataSurface.forget(); } /* static */ void CanvasChild::ReleaseDataShmemHolder(void* aClosure) { auto* shmemHolder = static_cast(aClosure); shmemHolder->Destroy(); } already_AddRefed CanvasChild::WrapSurface( const RefPtr& aSurface, int64_t aTextureId) { NS_ASSERT_OWNINGTHREAD(CanvasChild); if (!aSurface) { return nullptr; } return MakeAndAddRef(aTextureId, aSurface, this, mRecorder); } void CanvasChild::ReturnDataSurfaceShmem( already_AddRefed aDataSurfaceShmem) { RefPtr data = aDataSurfaceShmem; // We can only reuse the latest data surface shmem. if (data == mDataSurfaceShmem) { MOZ_ASSERT(!mDataSurfaceShmemAvailable); mDataSurfaceShmemAvailable = true; } } void CanvasChild::AttachSurface(const RefPtr& aSurface) { if (auto* surface = static_cast(aSurface.get())) { surface->AttachSurface(); } } void CanvasChild::DetachSurface(const RefPtr& aSurface) { if (auto* surface = static_cast(aSurface.get())) { surface->DetachSurface(); } } ipc::IPCResult CanvasChild::RecvNotifyRequiresRefresh(int64_t aTextureId) { auto it = mTextureInfo.find(aTextureId); if (it != mTextureInfo.end()) { it->second.mRequiresRefresh = true; } return IPC_OK(); } bool CanvasChild::RequiresRefresh(int64_t aTextureId) const { if (mBlocked) { return true; } auto it = mTextureInfo.find(aTextureId); if (it != mTextureInfo.end()) { return it->second.mRequiresRefresh; } return false; } ipc::IPCResult CanvasChild::RecvSnapshotShmem( int64_t aTextureId, Handle&& aShmemHandle, uint32_t aShmemSize, SnapshotShmemResolver&& aResolve) { auto it = mTextureInfo.find(aTextureId); if (it != mTextureInfo.end()) { auto shmem = MakeRefPtr(); if (NS_WARN_IF(!shmem->SetHandle(std::move(aShmemHandle), ipc::SharedMemory::RightsReadOnly)) || NS_WARN_IF(!shmem->Map(aShmemSize))) { shmem = nullptr; } else { it->second.mSnapshotShmem = std::move(shmem); } aResolve(true); } else { aResolve(false); } return IPC_OK(); } void CanvasChild::CleanupTexture(int64_t aTextureId) { mTextureInfo.erase(aTextureId); } } // namespace layers } // namespace mozilla