/* 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/CanvasShutdownManager.h"
#include "mozilla/gfx/DrawTargetRecording.h"
#include "mozilla/gfx/gfxVars.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/ipc/SharedMemoryHandle.h"
#include "mozilla/layers/CanvasDrawEventRecorder.h"
#include "mozilla/layers/ImageDataSerializer.h"
#include "mozilla/layers/SourceSurfaceSharedData.h"
#include "mozilla/AppShutdown.h"
#include "mozilla/Mutex.h"
#include "mozilla/StaticPrefs_gfx.h"
#include "nsIObserverService.h"
#include "nsICanvasRenderingContextInternal.h"
#include "RecordedCanvasEventImpl.h"

namespace mozilla {
namespace layers {

class RecorderHelpers final : public CanvasDrawEventRecorder::Helpers {
 public:
  NS_DECL_OWNINGTHREAD

  explicit RecorderHelpers(const RefPtr<CanvasChild>& aCanvasChild)
      : mCanvasChild(aCanvasChild) {}

  ~RecorderHelpers() override = default;

  bool InitTranslator(
      TextureType aTextureType, TextureType aWebglTextureType,
      gfx::BackendType aBackendType,
      ipc::MutableSharedMemoryHandle&& aReadHandle,
      nsTArray<ipc::ReadOnlySharedMemoryHandle>&& aBufferHandles,
      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), std::move(aReaderSem),
        std::move(aWriterSem));
  }

  bool AddBuffer(ipc::ReadOnlySharedMemoryHandle&& aBufferHandle) override {
    NS_ASSERT_OWNINGTHREAD(RecorderHelpers);
    if (!mCanvasChild) {
      return false;
    }
    return mCanvasChild->SendAddBuffer(std::move(aBufferHandle));
  }

  bool ReaderClosed() override {
    NS_ASSERT_OWNINGTHREAD(RecorderHelpers);
    if (!mCanvasChild) {
      return false;
    }
    return !mCanvasChild->CanSend() || AppShutdown::IsShutdownImpending();
  }

  bool RestartReader() override {
    NS_ASSERT_OWNINGTHREAD(RecorderHelpers);
    if (!mCanvasChild) {
      return false;
    }
    return mCanvasChild->SendRestartTranslation();
  }

  already_AddRefed<CanvasChild> GetCanvasChild() const override {
    RefPtr<CanvasChild> canvasChild(mCanvasChild);
    return canvasChild.forget();
  }

 private:
  const WeakPtr<CanvasChild> mCanvasChild;
};

// Limit the number of in-flight export surfaces
static Atomic<uint32_t> sCurrentExportSurfaces(0);
// Limit the memory used by in-flight export surfaces
static Atomic<size_t> sCurrentExportSurfaceMemory(0);

class SourceSurfaceCanvasRecording final : public gfx::SourceSurface {
 public:
  MOZ_DECLARE_REFCOUNTED_VIRTUAL_TYPENAME(SourceSurfaceCanvasRecording, final)

  SourceSurfaceCanvasRecording(
      const RemoteTextureOwnerId aTextureOwnerId,
      const RefPtr<gfx::SourceSurface>& aRecordedSuface,
      CanvasChild* aCanvasChild,
      const RefPtr<CanvasDrawEventRecorder>& aRecorder)
      : mTextureOwnerId(aTextureOwnerId),
        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;
    ReferencePtr exportID = mExportID;
    if (NS_IsMainThread()) {
      ReleaseOnMainThread(std::move(mRecorder), surfaceAlias,
                          std::move(mRecordedSurface), std::move(mCanvasChild),
                          exportID);
      return;
    }

    mRecorder->AddPendingDeletion(
        [recorder = std::move(mRecorder), surfaceAlias,
         aliasedSurface = std::move(mRecordedSurface),
         canvasChild = std::move(mCanvasChild), exportID]() mutable -> void {
          ReleaseOnMainThread(std::move(recorder), surfaceAlias,
                              std::move(aliasedSurface), std::move(canvasChild),
                              exportID);
        });
  }

  gfx::SurfaceType GetType() const final {
    return gfx::SurfaceType::CANVAS_RECORDING;
  }

  gfx::SurfaceType GetUnderlyingType() const final {
    return mRecordedSurface->GetType();
  }

  already_AddRefed<SourceSurface> GetUnderlyingSurface() final {
    return do_AddRef(mRecordedSurface);
  }

  gfx::IntSize GetSize() const final { return mRecordedSurface->GetSize(); }

  gfx::SurfaceFormat GetFormat() const final {
    return mRecordedSurface->GetFormat();
  }

  already_AddRefed<gfx::DataSourceSurface> GetDataSurface() final {
    MutexAutoLock lock(mDataSurfaceLock);
    EnsureDataSurfaceOnMainThread(lock);
    return do_AddRef(mDataSourceSurface);
  }

  void AttachSurface() { mDetached = false; }
  void DetachSurface(bool aInvalidate = false) {
    mDetached = true;

    if (aInvalidate) {
      MutexAutoLock lock(mDataSurfaceLock);
      InvalidateDataSurface(lock);
    }
  }

  already_AddRefed<gfx::SourceSurface> ExtractSubrect(
      const gfx::IntRect& aRect) final {
    return mRecordedSurface->ExtractSubrect(aRect);
  }

  static size_t GetExportSurfaceSize(gfx::SourceSurface* aSurface) {
    return ImageDataSerializer::ComputeRGBBufferSize(aSurface->GetSize(),
                                                     aSurface->GetFormat())
        .valueOr(0);
  }

  bool GetSurfaceDescriptor(SurfaceDescriptor& aDesc) final {
    if (!NS_IsMainThread()) {
      // Only allow recording surface upload optimization on main thread.
      return false;
    }
    static Atomic<uintptr_t> sNextExportID(0);
    if (!mExportID) {
      if (++sCurrentExportSurfaces >
          StaticPrefs::gfx_canvas_accelerated_max_export_surfaces()) {
        --sCurrentExportSurfaces;
        return false;
      }
      size_t bytes = GetExportSurfaceSize(mRecordedSurface);
      if ((sCurrentExportSurfaceMemory += bytes) >
          StaticPrefs::gfx_canvas_accelerated_max_export_surface_memory()) {
        --sCurrentExportSurfaces;
        sCurrentExportSurfaceMemory -= bytes;
        return false;
      }
      mExportID = gfx::ReferencePtr(++sNextExportID);
      mRecorder->RecordEvent(RecordedAddExportSurface(mExportID, this));
    }
    aDesc = SurfaceDescriptorCanvasSurface(
        static_cast<gfx::CanvasManagerChild*>(mCanvasChild->Manager())->Id(),
        mCanvasChild->Id(), uintptr_t(mExportID));
    return true;
  }

 private:
  void InvalidateDataSurface(const MutexAutoLock& aProofOfLock)
      MOZ_REQUIRES(mDataSurfaceLock) {
    // The data is about to be invalidated and must be copied before it is
    // modified.
    if (mDataSourceSurface && mMayInvalidate) {
      mDataSourceSurface =
          gfx::Factory::CopyDataSourceSurface(mDataSourceSurface);
      mMayInvalidate = false;
    }
  }

  void EnsureDataSurfaceOnMainThread(const MutexAutoLock& aProofOfLock)
      MOZ_REQUIRES(mDataSurfaceLock) {
    if (NS_IsMainThread()) {
      // The data can only be retrieved on the main thread.
      if (!mDataSourceSurface) {
        mDataSourceSurface = mCanvasChild->GetDataSurface(
            mTextureOwnerId, mRecordedSurface, mDetached, mMayInvalidate);
      }
    } else {
      // If data is going to be accessed on another thread, then copy the data
      // if necessary before access. This avoids the main thread accidentally
      // trying to invalidate the data surface while the other thread is still
      // accessing it.
      InvalidateDataSurface(aProofOfLock);
    }
  }

  // Used to ensure that clean-up that requires it is done on the main thread.
  static void ReleaseOnMainThread(RefPtr<CanvasDrawEventRecorder> aRecorder,
                                  ReferencePtr aSurfaceAlias,
                                  RefPtr<gfx::SourceSurface> aAliasedSurface,
                                  RefPtr<CanvasChild> aCanvasChild,
                                  ReferencePtr aExportID) {
    MOZ_ASSERT(NS_IsMainThread());

    aRecorder->RemoveStoredObject(aSurfaceAlias);
    aRecorder->RecordEvent(RecordedRemoveSurfaceAlias(aSurfaceAlias));
    if (aExportID) {
      aRecorder->RecordEvent(RecordedRemoveExportSurface(aExportID));
      --sCurrentExportSurfaces;
      size_t bytes = GetExportSurfaceSize(aAliasedSurface);
      sCurrentExportSurfaceMemory -= bytes;
    }
    aAliasedSurface = nullptr;
    aCanvasChild = nullptr;
    aRecorder = nullptr;
  }

  const RemoteTextureOwnerId mTextureOwnerId;
  RefPtr<gfx::SourceSurface> mRecordedSurface;
  RefPtr<CanvasChild> mCanvasChild;
  RefPtr<CanvasDrawEventRecorder> mRecorder;
  Mutex mDataSurfaceLock{"SourceSurfaceCanvasRecording::mDataSurfaceLock"};
  RefPtr<gfx::DataSourceSurface> mDataSourceSurface
      MOZ_GUARDED_BY(mDataSurfaceLock);
  bool mMayInvalidate MOZ_GUARDED_BY(mDataSurfaceLock) = false;
  bool mDetached = false;
  ReferencePtr mExportID;
};

class CanvasDataShmemHolder {
 public:
  CanvasDataShmemHolder(
      const std::shared_ptr<ipc::ReadOnlySharedMemoryMapping>& aShmem,
      CanvasChild* aCanvasChild)
      : mMutex("CanvasChild::DataShmemHolder::mMutex"),
        mShmem(aShmem),
        mCanvasChild(aCanvasChild) {}

  bool Init(dom::ThreadSafeWorkerRef* aWorkerRef) {
    if (!aWorkerRef) {
      return true;
    }

    RefPtr<dom::StrongWorkerRef> workerRef = dom::StrongWorkerRef::Create(
        aWorkerRef->Private(), "CanvasChild::DataShmemHolder",
        [this]() { DestroyWorker(); });
    if (NS_WARN_IF(!workerRef)) {
      return false;
    }

    MutexAutoLock lock(mMutex);
    mWorkerRef = MakeRefPtr<dom::ThreadSafeWorkerRef>(workerRef);
    return true;
  }

  void Destroy() {
    class DestroyRunnable final : public dom::WorkerThreadRunnable {
     public:
      explicit DestroyRunnable(CanvasDataShmemHolder* aShmemHolder)
          : dom::WorkerThreadRunnable("CanvasDataShmemHolder::Destroy"),
            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<DestroyRunnable>(this);
          dom::WorkerPrivate* worker = mWorkerRef->Private();
          mMutex.Unlock();
          task->Dispatch(worker);
          return;
        }
      } else if (!NS_IsMainThread()) {
        mMutex.Unlock();
        NS_DispatchToMainThread(NS_NewRunnableFunction(
            "CanvasDataShmemHolder::Destroy", [this]() { Destroy(); }));
        return;
      }

      mCanvasChild->ReturnDataSurfaceShmem(std::move(mShmem));
      mCanvasChild = nullptr;
      mWorkerRef = nullptr;
    }

    mMutex.Unlock();
    delete this;
  }

  void DestroyWorker() {
    MutexAutoLock lock(mMutex);
    mCanvasChild = nullptr;
    mWorkerRef = nullptr;
  }

 private:
  Mutex mMutex;
  std::shared_ptr<ipc::ReadOnlySharedMemoryMapping> mShmem;
  RefPtr<CanvasChild> mCanvasChild MOZ_GUARDED_BY(mMutex);
  RefPtr<dom::ThreadSafeWorkerRef> mWorkerRef MOZ_GUARDED_BY(mMutex);
};

CanvasChild::CanvasChild(dom::ThreadSafeWorkerRef* aWorkerRef)
    : mWorkerRef(aWorkerRef) {}

CanvasChild::~CanvasChild() { MOZ_ASSERT(!mWorkerRef); }

static void NotifyCanvasDeviceChanged() {
  nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
  if (obs) {
    obs->NotifyObservers(nullptr, "canvas-device-reset", nullptr);
  }
}

ipc::IPCResult CanvasChild::RecvNotifyDeviceReset(
    const nsTArray<RemoteTextureOwnerId>& aOwnerIds) {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);

  if (auto* manager = gfx::CanvasShutdownManager::MaybeGet()) {
    manager->OnRemoteCanvasReset(aOwnerIds);
  }

  mRecorder->RecordEvent(RecordedDeviceResetAcknowledged());
  return IPC_OK();
}

/* static */ bool CanvasChild::mDeactivated = false;

ipc::IPCResult CanvasChild::RecvDeactivate() {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);

  RefPtr<CanvasChild> self(this);
  mDeactivated = true;
  if (auto* cm = gfx::CanvasManagerChild::Get()) {
    cm->DeactivateCanvas();
  }
  NotifyCanvasDeviceChanged();
  return IPC_OK();
}

ipc::IPCResult CanvasChild::RecvBlockCanvas() {
  mBlocked = true;
  if (auto* cm = gfx::CanvasManagerChild::Get()) {
    cm->BlockCanvas();
  }
  return IPC_OK();
}

bool 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<CanvasDrawEventRecorder>(mWorkerRef);
    if (!recorder->Init(aTextureType, aWebglTextureType, backendType,
                        MakeUnique<RecorderHelpers>(this))) {
      return false;
    }

    mRecorder = recorder.forget();
  }

  if (NS_WARN_IF(mRecorder->GetTextureType() != aTextureType)) {
    // The recorder has already been initialized with a different type. This can
    // happen if there is a device reset or fallback that causes a switch to a
    // different unaccelerated texture type (i.e. unknown). In that case, just
    // fall back to non-remote rendering.
    return false;
  }

  EnsureDataSurfaceShmem(aSize, aFormat);

  return true;
}

void CanvasChild::ActorDestroy(ActorDestroyReason aWhy) {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);

  if (mRecorder) {
    mRecorder->DetachResources();
  }
  mTextureInfo.clear();
}

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());
    }
  }

  // If we are continuously drawing/recording, then we need to periodically
  // flush our external surface/image references, to ensure they actually get
  // freed on a timely basis.
  if (mRecorder) {
    mRecorder->ClearProcessedExternalSurfaces();
    mRecorder->ClearProcessedExternalImages();
  }

  ++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();
    // Notify CanvasTranslator it is dormant.
    SendDropFreeBuffersWhenDormant();
  }
}

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() && mTextureInfo.empty());
}

already_AddRefed<gfx::DrawTargetRecording> CanvasChild::CreateDrawTarget(
    const RemoteTextureOwnerId& aTextureOwnerId, gfx::IntSize aSize,
    gfx::SurfaceFormat aFormat) {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);
  MOZ_ASSERT(mTextureInfo.find(aTextureOwnerId) == mTextureInfo.end());

  if (!mRecorder) {
    return nullptr;
  }

  RefPtr<gfx::DrawTarget> dummyDt = gfx::Factory::CreateDrawTarget(
      gfx::BackendType::SKIA, gfx::IntSize(1, 1), aFormat);
  RefPtr<gfx::DrawTargetRecording> dt = MakeAndAddRef<gfx::DrawTargetRecording>(
      mRecorder, aTextureOwnerId, dummyDt, aSize);
  dt->SetOptimizeTransform(true);

  mTextureInfo.insert({aTextureOwnerId, {}});

  return dt.forget();
}

size_t CanvasChild::SizeOfDataSurfaceShmem(gfx::IntSize aSize,
                                           gfx::SurfaceFormat aFormat) {
  if (!mRecorder) {
    return 0;
  }
  Maybe<uint32_t> sizeRequired =
      ImageDataSerializer::ComputeRGBBufferSize(aSize, aFormat);
  return sizeRequired.isSome()
             ? ipc::shared_memory::PageAlignedSize(size_t(sizeRequired.value()))
             : 0;
}

bool CanvasChild::ShouldGrowDataSurfaceShmem(size_t aSizeRequired) {
  return aSizeRequired > 0 && (!mDataSurfaceShmemAvailable ||
                               mDataSurfaceShmem->Size() < aSizeRequired);
}

bool CanvasChild::EnsureDataSurfaceShmem(size_t aSizeRequired) {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);

  if (!aSizeRequired) {
    return false;
  }

  if (ShouldGrowDataSurfaceShmem(aSizeRequired)) {
    RecordEvent(RecordedPauseTranslation());
    auto shmemHandle = ipc::shared_memory::Create(aSizeRequired);
    if (!shmemHandle) {
      return false;
    }

    auto roMapping = shmemHandle.AsReadOnly().Map();
    if (!roMapping) {
      return false;
    }

    auto id = ++mNextDataSurfaceShmemId;
    if (!id) {
      // If ids overflow, ensure that zero is reserved.
      id = ++mNextDataSurfaceShmemId;
    }
    if (!SendSetDataSurfaceBuffer(id, std::move(shmemHandle))) {
      return false;
    }

    mDataSurfaceShmem = std::make_shared<ipc::ReadOnlySharedMemoryMapping>(
        std::move(roMapping));
    mDataSurfaceShmemAvailable = true;
  }

  MOZ_ASSERT(mDataSurfaceShmemAvailable);
  return true;
}

bool CanvasChild::EnsureDataSurfaceShmem(gfx::IntSize aSize,
                                         gfx::SurfaceFormat aFormat) {
  size_t sizeRequired = SizeOfDataSurfaceShmem(aSize, aFormat);
  if (!sizeRequired) {
    return false;
  }

  return EnsureDataSurfaceShmem(sizeRequired);
}

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<gfx::DataSourceSurface> CanvasChild::GetDataSurface(
    const RemoteTextureOwnerId aTextureOwnerId,
    const gfx::SourceSurface* aSurface, bool aDetached, bool& aMayInvalidate) {
  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);
  if (stride.isNothing()) {
    return nullptr;
  }

  size_t sizeRequired = SizeOfDataSurfaceShmem(ssSize, ssFormat);
  if (!sizeRequired) {
    return nullptr;
  }

  // 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
    // to use that directly instead of allocating a new shmem for retrieval.
    // Also check that it meets the size required.
    auto it = mTextureInfo.find(aTextureOwnerId);
    if (it != mTextureInfo.end() && it->second.mSnapshotShmem &&
        !NS_WARN_IF(sizeRequired > it->second.mSnapshotShmem->Size())) {
      const auto* shmemPtr = it->second.mSnapshotShmem->DataAs<uint8_t>();
      MOZ_ASSERT(shmemPtr);
      mRecorder->RecordEvent(RecordedPrepareShmem(aTextureOwnerId));
      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;
      }
      // We can cast away the const of `shmemPtr` to match the call because the
      // DataSourceSurface will not be written to.
      RefPtr<gfx::DataSourceSurface> dataSurface =
          gfx::Factory::CreateWrappingDataSourceSurface(
              const_cast<uint8_t*>(shmemPtr), stride.value(), ssSize, ssFormat,
              ReleaseDataShmemHolder, closure);
      aMayInvalidate = true;
      return dataSurface.forget();
    }
  }

  RecordEvent(RecordedCacheDataSurface(aSurface));

  if (!EnsureDataSurfaceShmem(sizeRequired)) {
    return nullptr;
  }

  RecordEvent(RecordedGetDataForSurface(mNextDataSurfaceShmemId, 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;

  const auto* data = mDataSurfaceShmem->DataAs<uint8_t>();

  // We can cast away the const of `data` to match the call because the
  // DataSourceSurface will not be written to.
  RefPtr<gfx::DataSourceSurface> dataSurface =
      gfx::Factory::CreateWrappingDataSourceSurface(
          const_cast<uint8_t*>(data), stride.value(), ssSize, ssFormat,
          ReleaseDataShmemHolder, closure);
  aMayInvalidate = false;
  return dataSurface.forget();
}

/* static */ void CanvasChild::ReleaseDataShmemHolder(void* aClosure) {
  auto* shmemHolder = static_cast<CanvasDataShmemHolder*>(aClosure);
  shmemHolder->Destroy();
}

already_AddRefed<gfx::SourceSurface> CanvasChild::WrapSurface(
    const RefPtr<gfx::SourceSurface>& aSurface,
    const RemoteTextureOwnerId aTextureOwnerId) {
  NS_ASSERT_OWNINGTHREAD(CanvasChild);

  if (!aSurface) {
    return nullptr;
  }

  return MakeAndAddRef<SourceSurfaceCanvasRecording>(aTextureOwnerId, aSurface,
                                                     this, mRecorder);
}

void CanvasChild::ReturnDataSurfaceShmem(
    std::shared_ptr<ipc::ReadOnlySharedMemoryMapping>&& aDataSurfaceShmem) {
  // We can only reuse the latest data surface shmem.
  if (aDataSurfaceShmem == mDataSurfaceShmem) {
    MOZ_ASSERT(!mDataSurfaceShmemAvailable);
    mDataSurfaceShmemAvailable = true;
  }
}

void CanvasChild::AttachSurface(const RefPtr<gfx::SourceSurface>& aSurface) {
  if (auto* surface =
          static_cast<SourceSurfaceCanvasRecording*>(aSurface.get())) {
    surface->AttachSurface();
  }
}

void CanvasChild::DetachSurface(const RefPtr<gfx::SourceSurface>& aSurface,
                                bool aInvalidate) {
  if (auto* surface =
          static_cast<SourceSurfaceCanvasRecording*>(aSurface.get())) {
    surface->DetachSurface(aInvalidate);
  }
}

ipc::IPCResult CanvasChild::RecvNotifyRequiresRefresh(
    const RemoteTextureOwnerId aTextureOwnerId) {
  auto it = mTextureInfo.find(aTextureOwnerId);
  if (it != mTextureInfo.end()) {
    it->second.mRequiresRefresh = true;
  }
  return IPC_OK();
}

bool CanvasChild::RequiresRefresh(
    const RemoteTextureOwnerId aTextureOwnerId) const {
  if (mBlocked) {
    return true;
  }
  auto it = mTextureInfo.find(aTextureOwnerId);
  if (it != mTextureInfo.end()) {
    return it->second.mRequiresRefresh;
  }
  return false;
}

ipc::IPCResult CanvasChild::RecvSnapshotShmem(
    const RemoteTextureOwnerId aTextureOwnerId,
    ipc::ReadOnlySharedMemoryHandle&& aShmemHandle,
    SnapshotShmemResolver&& aResolve) {
  auto it = mTextureInfo.find(aTextureOwnerId);
  if (it != mTextureInfo.end()) {
    auto shmem = aShmemHandle.Map();
    if (NS_WARN_IF(!shmem)) {
      shmem = nullptr;
    } else {
      it->second.mSnapshotShmem =
          std::make_shared<ipc::ReadOnlySharedMemoryMapping>(std::move(shmem));
    }
    aResolve(true);
  } else {
    aResolve(false);
  }
  return IPC_OK();
}

ipc::IPCResult CanvasChild::RecvNotifyTextureDestruction(
    const RemoteTextureOwnerId aTextureOwnerId) {
  auto it = mTextureInfo.find(aTextureOwnerId);
  if (it == mTextureInfo.end()) {
    MOZ_ASSERT(!CanSend());
    return IPC_OK();
  }

  mTextureInfo.erase(aTextureOwnerId);
  return IPC_OK();
}

already_AddRefed<gfx::SourceSurface> CanvasChild::SnapshotExternalCanvas(
    gfx::DrawTargetRecording* aTarget,
    nsICanvasRenderingContextInternal* aCanvas,
    mozilla::ipc::IProtocol* aActor) {
  // SnapshotExternalCanvas is only valid to use if using Accelerated Canvas2D
  // with the pending events queue enabled. This ensures WebGL and AC2D are
  // running under the same thread, and that events can be paused or resumed
  // while synchronizing between WebGL and AC2D.
  if (!gfx::gfxVars::UseAcceleratedCanvas2D() ||
      !StaticPrefs::gfx_canvas_remote_use_canvas_translator_event_AtStartup()) {
    return nullptr;
  }

  gfx::SurfaceFormat format = aCanvas->GetIsOpaque()
                                  ? gfx::SurfaceFormat::B8G8R8X8
                                  : gfx::SurfaceFormat::B8G8R8A8;
  gfx::IntSize size(aCanvas->GetWidth(), aCanvas->GetHeight());
  // Create a source sourface that will be associated with the snapshot.
  RefPtr<gfx::SourceSurface> surface =
      aTarget->CreateExternalSourceSurface(size, format);
  if (!surface) {
    return nullptr;
  }

  // Pause translation until the sync-id identifying the snapshot is received.
  uint64_t syncId = ++mLastSyncId;
  mRecorder->RecordEvent(RecordedAwaitTranslationSync(syncId));

  // Flush WebGL to cause any IPDL messages to get sent at this sync point.
  aCanvas->SyncSnapshot();

  // Once the IPDL message is sent to generate the snapshot, resolve the sync-id
  // to a surface in the recording stream. The AwaitTranslationSync above will
  // ensure this event is not translated until the snapshot is generated first.
  mRecorder->RecordEvent(aTarget,
                         RecordedResolveExternalSnapshot(
                             syncId, gfx::ReferencePtr(surface), size, format));

  uint32_t managerId = static_cast<gfx::CanvasManagerChild*>(Manager())->Id();
  ActorId canvasId = aActor->Id();

  // Actually send the request via IPDL to snapshot the external WebGL canvas.
  SendSnapshotExternalCanvas(syncId, managerId, canvasId);

  return surface.forget();
}

}  // namespace layers
}  // namespace mozilla
