/* 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 "Queue.h"

#include <algorithm>

#include "CommandBuffer.h"
#include "CommandEncoder.h"
#include "ExternalTexture.h"
#include "Utility.h"
#include "ipc/WebGPUChild.h"
#include "mozilla/Casting.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/dom/BufferSourceBinding.h"
#include "mozilla/dom/HTMLCanvasElement.h"
#include "mozilla/dom/HTMLImageElement.h"
#include "mozilla/dom/ImageBitmap.h"
#include "mozilla/dom/OffscreenCanvas.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/PromiseNativeHandler.h"
#include "mozilla/dom/UnionTypes.h"
#include "mozilla/dom/WebGLTexelConversions.h"
#include "mozilla/dom/WebGLTypes.h"
#include "mozilla/dom/WebGPUBinding.h"
#include "mozilla/ipc/SharedMemoryHandle.h"
#include "mozilla/ipc/SharedMemoryMapping.h"
#include "nsLayoutUtils.h"

namespace mozilla::webgpu {

GPU_IMPL_CYCLE_COLLECTION(Queue, mParent)
GPU_IMPL_JS_WRAP(Queue)

Queue::Queue(Device* const aParent, RawId aId)
    : ObjectBase(aParent->GetChild(), aId, ffi::wgpu_client_drop_queue),
      ChildOf(aParent) {}

Queue::~Queue() = default;

struct ExternalTextureWorkDoneHandler : dom::PromiseNativeHandler {
  NS_DECL_ISUPPORTS

  explicit ExternalTextureWorkDoneHandler(
      nsTArray<RefPtr<ExternalTexture>>&& aExternalTextures,
      uint64_t aSubmissionId)
      : mExternalTextures(std::move(aExternalTextures)),
        mSubmissionId(aSubmissionId) {}

  void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aRv) override {
    for (const auto& externalTexture : mExternalTextures) {
      externalTexture->OnSubmittedWorkDone(mSubmissionId);
    }
  }
  void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
                        ErrorResult& aRv) override {
    MOZ_ASSERT_UNREACHABLE("Work done promise should not be rejected");
  }

 private:
  ~ExternalTextureWorkDoneHandler() = default;
  // We must hold a strong reference to the external textures to ensure that
  // they are not released before all work involving them is done.
  const nsTArray<RefPtr<ExternalTexture>> mExternalTextures;
  const uint64_t mSubmissionId;
};

NS_IMPL_ISUPPORTS0(ExternalTextureWorkDoneHandler)

void Queue::Submit(
    const dom::Sequence<OwningNonNull<CommandBuffer>>& aCommandBuffers) {
  nsTArray<RawId> list(aCommandBuffers.Length());
  nsTArray<RefPtr<ExternalTexture>> externalTextures;
  for (uint32_t i = 0; i < aCommandBuffers.Length(); ++i) {
    auto idMaybe = aCommandBuffers[i]->Commit();

    // Generate a validation error if any external texture used by any command
    // buffer is expired. Technically this is a Device timeline step, but since
    // the external texture's expired state is only set on the content timeline
    // it is functionally equivalent to check here and raise any error on the
    // device timeline. A compromised content process could skip this step, but
    // equally it could skip setting the external texture's expired state even
    // if this check were performed on the server side.
    // https://www.w3.org/TR/webgpu/#dom-gpuqueue-submit
    for (const auto& externalTexture :
         aCommandBuffers[i]->GetExternalTextures()) {
      if (externalTexture->IsExpired()) {
        ffi::wgpu_report_validation_error(GetClient(), mParent->GetId(),
                                          "External texture is expired");
        return;
      }
    }
    externalTextures.AppendElements(aCommandBuffers[i]->GetExternalTextures());

    if (idMaybe) {
      list.AppendElement(idMaybe);
    }
  }

  nsTArray<RawId> externalTextureSourceIds;
  for (auto& externalTexture : externalTextures) {
    externalTextureSourceIds.AppendElement(externalTexture->Source()->GetId());
  }

  GetChild()->QueueSubmit(GetId(), mParent->GetId(), list,
                          externalTextureSourceIds);

  if (!externalTextures.IsEmpty()) {
    for (const auto& externalTexture : externalTextures) {
      externalTexture->OnSubmit(mNextExternalTextureSubmissionIndex);
    }
    ErrorResult rv;
    RefPtr<dom::Promise> promise = OnSubmittedWorkDone(rv);
    // Without this promise we have no way of knowing when work involving the
    // external textures is done. This would lead to us holding on to the
    // external texture's resources indefinitely, which we don't want.
    // The alternative of releasing the resources immediately is unacceptable
    // while there is still pending work, so just crash.
    MOZ_RELEASE_ASSERT(promise);
    RefPtr<ExternalTextureWorkDoneHandler> handler =
        new ExternalTextureWorkDoneHandler(std::move(externalTextures),
                                           mNextExternalTextureSubmissionIndex);
    promise->AppendNativeHandler(handler);

    mNextExternalTextureSubmissionIndex++;
  }
}

already_AddRefed<dom::Promise> Queue::OnSubmittedWorkDone(ErrorResult& aRv) {
  RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
  if (NS_WARN_IF(aRv.Failed())) {
    return nullptr;
  }

  ffi::wgpu_client_on_submitted_work_done(GetClient(), mParent->GetId(),
                                          GetId());

  GetChild()->EnqueueOnSubmittedWorkDonePromise(GetId(), promise);

  return promise.forget();
}

void Queue::WriteBuffer(
    const Buffer& aBuffer, uint64_t aBufferOffset,
    const dom::MaybeSharedArrayBufferOrMaybeSharedArrayBufferView& aData,
    uint64_t aDataOffset, const dom::Optional<uint64_t>& aSize,
    ErrorResult& aRv) {
  if (!aBuffer.GetId()) {
    // Invalid buffers are unknown to the parent -- don't try to write
    // to them.
    return;
  }

  size_t elementByteSize = 1;
  if (aData.IsArrayBufferView()) {
    auto type = aData.GetAsArrayBufferView().Type();
    if (type != JS::Scalar::MaxTypedArrayViewType) {
      elementByteSize = byteSize(type);
    }
  }
  dom::ProcessTypedArraysFixed(aData, [&, elementByteSize](
                                          const Span<const uint8_t>& aData) {
    uint64_t byteLength = aData.Length();

    auto checkedByteOffset =
        CheckedInt<uint64_t>(aDataOffset) * elementByteSize;
    if (!checkedByteOffset.isValid()) {
      aRv.ThrowOperationError("offset x element size overflows");
      return;
    }
    auto offset = checkedByteOffset.value();

    size_t size;
    if (aSize.WasPassed()) {
      const auto checkedByteSize =
          CheckedInt<size_t>(aSize.Value()) * elementByteSize;
      if (!checkedByteSize.isValid()) {
        aRv.ThrowOperationError("write size x element size overflows");
        return;
      }
      size = checkedByteSize.value();
    } else {
      const auto checkedByteSize = CheckedInt<size_t>(byteLength) - offset;
      if (!checkedByteSize.isValid()) {
        aRv.ThrowOperationError("data byte length - offset underflows");
        return;
      }
      size = checkedByteSize.value();
    }

    auto checkedByteEnd = CheckedInt<uint64_t>(offset) + size;
    if (!checkedByteEnd.isValid() || checkedByteEnd.value() > byteLength) {
      aRv.ThrowOperationError(
          nsPrintfCString("Wrong data size %" PRIuPTR, size));
      return;
    }

    if (size % 4 != 0) {
      aRv.ThrowOperationError("Byte size must be a multiple of 4");
      return;
    }

    if (size < 1024) {
      ipc::ByteBuf bb{};
      bb.Allocate(size);
      memcpy(bb.mData, aData.Elements() + offset, size);
      auto data_buffer_index = GetChild()->QueueDataBuffer(std::move(bb));
      ffi::wgpu_queue_write_buffer_inline(GetClient(), mParent->GetId(),
                                          GetId(), aBuffer.GetId(),
                                          aBufferOffset, data_buffer_index);
      return;
    }

    mozilla::ipc::MutableSharedMemoryHandle handle;
    if (size != 0) {
      handle = mozilla::ipc::shared_memory::Create(size);
      auto mapping = handle.Map();
      if (!handle || !mapping) {
        aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
        return;
      }

      memcpy(mapping.DataAs<uint8_t>(), aData.Elements() + offset, size);
    }
    auto shmem_handle_index = GetChild()->QueueShmemHandle(std::move(handle));
    ffi::wgpu_queue_write_buffer_via_shmem(GetClient(), mParent->GetId(),
                                           GetId(), aBuffer.GetId(),
                                           aBufferOffset, shmem_handle_index);
  });
}

static CheckedInt<size_t> ComputeApproxSize(
    const dom::GPUTexelCopyTextureInfo& aDestination,
    const dom::GPUTexelCopyBufferLayout& aDataLayout,
    const ffi::WGPUExtent3d& extent,
    const ffi::WGPUTextureFormatBlockInfo& info) {
  // The spec's algorithm for [validating linear texture data][vltd] computes
  // an exact size for the transfer. wgpu implements the algorithm and will
  // fully validate the operation as described in the spec.
  //
  // Here, we just want to avoid copying excessive amounts of data in the case
  // where the transfer will use only a small portion of the buffer. So we
  // compute an approximation that will be at least the actual transfer size
  // for any valid request. Then we copy the smaller of the approximated size
  // or the remainder of the buffer.
  //
  // [vltd]:
  // https://www.w3.org/TR/webgpu/#abstract-opdef-validating-linear-texture-data

  // VLTD requires that width/height are multiples of the block size.
  auto widthInBlocks = extent.width / info.width;
  auto heightInBlocks = extent.height / info.height;
  auto bytesInLastRow = CheckedInt<size_t>(widthInBlocks) * info.copy_size;

  // VLTD requires bytesPerRow present if heightInBlocks > 1.
  auto bytesPerRow = CheckedInt<size_t>(aDataLayout.mBytesPerRow.WasPassed()
                                            ? aDataLayout.mBytesPerRow.Value()
                                            : bytesInLastRow);

  if (extent.depth_or_array_layers > 1) {
    // VLTD requires rowsPerImage present if layers > 1
    auto rowsPerImage = aDataLayout.mRowsPerImage.WasPassed()
                            ? aDataLayout.mRowsPerImage.Value()
                            : heightInBlocks;
    return bytesPerRow * rowsPerImage * extent.depth_or_array_layers;
  } else {
    return bytesPerRow * heightInBlocks;
  }
}

void Queue::WriteTexture(
    const dom::GPUTexelCopyTextureInfo& aDestination,
    const dom::MaybeSharedArrayBufferOrMaybeSharedArrayBufferView& aData,
    const dom::GPUTexelCopyBufferLayout& aDataLayout,
    const dom::GPUExtent3D& aSize, ErrorResult& aRv) {
  ffi::WGPUTexelCopyTextureInfo copyView = {};
  CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, &copyView);
  ffi::WGPUTexelCopyBufferLayout dataLayout = {};
  CommandEncoder::ConvertTextureDataLayoutToFFI(aDataLayout, &dataLayout);
  dataLayout.offset = 0;  // our Shmem has the contents starting from 0.
  ffi::WGPUExtent3d extent = {};
  ConvertExtent3DToFFI(aSize, &extent);

  auto format = ConvertTextureFormat(aDestination.mTexture->Format());
  auto aspect = ConvertTextureAspect(aDestination.mAspect);
  ffi::WGPUTextureFormatBlockInfo info = {};
  bool valid = ffi::wgpu_texture_format_get_block_info(format, aspect, &info);
  CheckedInt<size_t> approxSize;
  if (valid) {
    approxSize = ComputeApproxSize(aDestination, aDataLayout, extent, info);
  } else {
    // This happens when the caller does not indicate a single aspect to
    // target in a multi-aspect texture. It needs to be validated on the
    // device timeline, so proceed without an estimated size for now
    approxSize = CheckedInt<size_t>(SIZE_MAX) + 1;
  }

  dom::ProcessTypedArraysFixed(aData, [&](const Span<const uint8_t>& aData) {
    const auto checkedSize =
        CheckedInt<size_t>(aData.Length()) - aDataLayout.mOffset;
    size_t size;
    if (checkedSize.isValid() && approxSize.isValid()) {
      size = std::min(checkedSize.value(), approxSize.value());
    } else if (checkedSize.isValid()) {
      size = checkedSize.value();
    } else {
      // CheckedSize is invalid when the caller-provided offset was past the
      // end of their buffer. Maintain that condition, and fail the operation
      // on the device timeline.
      dataLayout.offset = 1;
      size = 0;
    }

    mozilla::ipc::MutableSharedMemoryHandle handle;
    if (size != 0) {
      handle = mozilla::ipc::shared_memory::Create(size);
      auto mapping = handle.Map();
      if (!handle || !mapping) {
        aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
        return;
      }

      memcpy(mapping.DataAs<uint8_t>(), aData.Elements() + aDataLayout.mOffset,
             size);
    } else {
      handle = mozilla::ipc::MutableSharedMemoryHandle();
    }

    auto shmem_handle_index = GetChild()->QueueShmemHandle(std::move(handle));
    ffi::wgpu_queue_write_texture_via_shmem(GetClient(), mParent->GetId(),
                                            GetId(), copyView, dataLayout,
                                            extent, shmem_handle_index);
  });
}

static WebGLTexelFormat ToWebGLTexelFormat(gfx::SurfaceFormat aFormat) {
  switch (aFormat) {
    case gfx::SurfaceFormat::B8G8R8A8:
    case gfx::SurfaceFormat::B8G8R8X8:
      return WebGLTexelFormat::BGRA8;
    case gfx::SurfaceFormat::R8G8B8A8:
    case gfx::SurfaceFormat::R8G8B8X8:
      return WebGLTexelFormat::RGBA8;
    default:
      return WebGLTexelFormat::FormatNotSupportingAnyConversion;
  }
}

static WebGLTexelFormat ToWebGLTexelFormat(dom::GPUTextureFormat aFormat) {
  // TODO: We need support for Rbg10a2unorm as well.
  switch (aFormat) {
    case dom::GPUTextureFormat::R8unorm:
      return WebGLTexelFormat::R8;
    case dom::GPUTextureFormat::R16float:
      return WebGLTexelFormat::R16F;
    case dom::GPUTextureFormat::R32float:
      return WebGLTexelFormat::R32F;
    case dom::GPUTextureFormat::Rg8unorm:
      return WebGLTexelFormat::RG8;
    case dom::GPUTextureFormat::Rg16float:
      return WebGLTexelFormat::RG16F;
    case dom::GPUTextureFormat::Rg32float:
      return WebGLTexelFormat::RG32F;
    case dom::GPUTextureFormat::Rgba8unorm:
    case dom::GPUTextureFormat::Rgba8unorm_srgb:
      return WebGLTexelFormat::RGBA8;
    case dom::GPUTextureFormat::Bgra8unorm:
    case dom::GPUTextureFormat::Bgra8unorm_srgb:
      return WebGLTexelFormat::BGRA8;
    case dom::GPUTextureFormat::Rgba16float:
      return WebGLTexelFormat::RGBA16F;
    case dom::GPUTextureFormat::Rgba32float:
      return WebGLTexelFormat::RGBA32F;
    default:
      return WebGLTexelFormat::FormatNotSupportingAnyConversion;
  }
}

void Queue::CopyExternalImageToTexture(
    const dom::GPUCopyExternalImageSourceInfo& aSource,
    const dom::GPUCopyExternalImageDestInfo& aDestination,
    const dom::GPUExtent3D& aCopySize, ErrorResult& aRv) {
  if (aSource.mOrigin.IsRangeEnforcedUnsignedLongSequence()) {
    auto seq = aSource.mOrigin.GetAsRangeEnforcedUnsignedLongSequence();
    if (seq.Length() > 2) {
      aRv.ThrowTypeError("`origin` must have a sequence size of 2 or less");
      return;
    }
  }

  const auto dstFormat = ToWebGLTexelFormat(aDestination.mTexture->Format());
  if (dstFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) {
    aRv.ThrowInvalidStateError("Unsupported destination format");
    return;
  }

  const uint32_t surfaceFlags = nsLayoutUtils::SFE_ALLOW_NON_PREMULT;
  SurfaceFromElementResult sfeResult;
  switch (aSource.mSource.GetType()) {
    case decltype(aSource.mSource)::Type::eImageBitmap: {
      const auto& bitmap = aSource.mSource.GetAsImageBitmap();
      if (bitmap->IsClosed()) {
        aRv.ThrowInvalidStateError("Detached ImageBitmap");
        return;
      }

      sfeResult = nsLayoutUtils::SurfaceFromImageBitmap(bitmap, surfaceFlags);
      break;
    }
    case decltype(aSource.mSource)::Type::eHTMLImageElement: {
      const auto& image = aSource.mSource.GetAsHTMLImageElement();
      if (image->NaturalWidth() == 0 || image->NaturalHeight() == 0) {
        aRv.ThrowInvalidStateError("Zero-sized HTMLImageElement");
        return;
      }

      sfeResult = nsLayoutUtils::SurfaceFromElement(image, surfaceFlags);
      break;
    }
    case decltype(aSource.mSource)::Type::eHTMLCanvasElement: {
      MOZ_ASSERT(NS_IsMainThread());

      const auto& canvas = aSource.mSource.GetAsHTMLCanvasElement();
      if (canvas->Width() == 0 || canvas->Height() == 0) {
        aRv.ThrowInvalidStateError("Zero-sized HTMLCanvasElement");
        return;
      }

      sfeResult = nsLayoutUtils::SurfaceFromElement(canvas, surfaceFlags);
      break;
    }
    case decltype(aSource.mSource)::Type::eOffscreenCanvas: {
      const auto& canvas = aSource.mSource.GetAsOffscreenCanvas();
      if (canvas->Width() == 0 || canvas->Height() == 0) {
        aRv.ThrowInvalidStateError("Zero-sized OffscreenCanvas");
        return;
      }

      sfeResult =
          nsLayoutUtils::SurfaceFromOffscreenCanvas(canvas, surfaceFlags);
      break;
    }
  }

  if (!sfeResult.mCORSUsed) {
    nsIGlobalObject* global = mParent->GetRelevantGlobal();
    nsIPrincipal* dstPrincipal = global ? global->PrincipalOrNull() : nullptr;
    if (!sfeResult.mPrincipal || !dstPrincipal ||
        !dstPrincipal->Subsumes(sfeResult.mPrincipal)) {
      aRv.ThrowSecurityError("Cross-origin elements require CORS!");
      return;
    }
  }

  if (sfeResult.mIsWriteOnly) {
    aRv.ThrowSecurityError("Write only source data not supported!");
    return;
  }

  RefPtr<gfx::SourceSurface> surface = sfeResult.GetSourceSurface();
  if (!surface) {
    aRv.ThrowInvalidStateError("No surface available from source");
    return;
  }

  RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
  if (!dataSurface) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  bool srcPremultiplied;
  switch (sfeResult.mAlphaType) {
    case gfxAlphaType::Premult:
      srcPremultiplied = true;
      break;
    case gfxAlphaType::NonPremult:
      srcPremultiplied = false;
      break;
    case gfxAlphaType::Opaque:
      // No (un)premultiplication necessary so match the output.
      srcPremultiplied = aDestination.mPremultipliedAlpha;
      break;
  }

  const auto surfaceFormat = dataSurface->GetFormat();
  const auto srcFormat = ToWebGLTexelFormat(surfaceFormat);
  if (srcFormat == WebGLTexelFormat::FormatNotSupportingAnyConversion) {
    gfxCriticalError() << "Unsupported surface format from source "
                       << surfaceFormat;
    MOZ_CRASH();
  }

  gfx::DataSourceSurface::ScopedMap map(dataSurface,
                                        gfx::DataSourceSurface::READ);
  if (!map.IsMapped()) {
    aRv.ThrowInvalidStateError("Cannot map surface from source");
    return;
  }

  ffi::WGPUExtent3d extent = {};
  ConvertExtent3DToFFI(aCopySize, &extent);
  if (extent.depth_or_array_layers > 1) {
    aRv.ThrowOperationError("Depth is greater than 1");
    return;
  }

  uint32_t srcOriginX;
  uint32_t srcOriginY;
  if (aSource.mOrigin.IsRangeEnforcedUnsignedLongSequence()) {
    const auto& seq = aSource.mOrigin.GetAsRangeEnforcedUnsignedLongSequence();
    srcOriginX = seq.Length() > 0 ? seq[0] : 0;
    srcOriginY = seq.Length() > 1 ? seq[1] : 0;
  } else if (aSource.mOrigin.IsGPUOrigin2DDict()) {
    const auto& dict = aSource.mOrigin.GetAsGPUOrigin2DDict();
    srcOriginX = dict.mX;
    srcOriginY = dict.mY;
  } else {
    MOZ_CRASH("Unexpected origin type!");
  }

  const auto checkedMaxWidth = CheckedInt<uint32_t>(srcOriginX) + extent.width;
  const auto checkedMaxHeight =
      CheckedInt<uint32_t>(srcOriginY) + extent.height;
  if (!checkedMaxWidth.isValid() || !checkedMaxHeight.isValid()) {
    aRv.ThrowOperationError("Offset and copy size exceed integer bounds");
    return;
  }

  const gfx::IntSize surfaceSize = dataSurface->GetSize();
  const auto surfaceWidth = AssertedCast<uint32_t>(surfaceSize.width);
  const auto surfaceHeight = AssertedCast<uint32_t>(surfaceSize.height);
  if (surfaceWidth < checkedMaxWidth.value() ||
      surfaceHeight < checkedMaxHeight.value()) {
    aRv.ThrowOperationError("Offset and copy size exceed surface bounds");
    return;
  }

  const auto dstWidth = extent.width;
  const auto dstHeight = extent.height;
  if (dstWidth == 0 || dstHeight == 0) {
    aRv.ThrowOperationError("Destination size is empty");
    return;
  }

  if (!aDestination.mTexture->mBytesPerBlock) {
    // TODO(bug 1781071) This should emmit a GPUValidationError on the device
    // timeline.
    aRv.ThrowInvalidStateError("Invalid destination format");
    return;
  }

  // Note: This assumes bytes per block == bytes per pixel which is the case
  // here because the spec only allows non-compressed texture formats for the
  // destination.
  const auto dstStride = CheckedInt<uint32_t>(extent.width) *
                         aDestination.mTexture->mBytesPerBlock.value();
  const auto dstByteLength = dstStride * extent.height;
  if (!dstStride.isValid() || !dstByteLength.isValid()) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  auto handle = mozilla::ipc::shared_memory::Create(dstByteLength.value());
  auto mapping = handle.Map();
  if (!handle || !mapping) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return;
  }

  const int32_t pixelSize = gfx::BytesPerPixel(surfaceFormat);
  auto* dstBegin = mapping.DataAs<uint8_t>();
  const auto* srcBegin =
      map.GetData() + srcOriginX * pixelSize + srcOriginY * map.GetStride();
  const auto srcOriginPos = gl::OriginPos::TopLeft;
  const auto srcStride = AssertedCast<uint32_t>(map.GetStride());
  const auto dstOriginPos =
      aSource.mFlipY ? gl::OriginPos::BottomLeft : gl::OriginPos::TopLeft;
  bool wasTrivial;

  auto dstStrideVal = dstStride.value();

  if (!ConvertImage(dstWidth, dstHeight, srcBegin, srcStride, srcOriginPos,
                    srcFormat, srcPremultiplied, dstBegin, dstStrideVal,
                    dstOriginPos, dstFormat, aDestination.mPremultipliedAlpha,
                    dom::PredefinedColorSpace::Srgb,
                    dom::PredefinedColorSpace::Srgb, &wasTrivial)) {
    MOZ_ASSERT_UNREACHABLE("ConvertImage failed!");
    aRv.ThrowInvalidStateError(
        nsPrintfCString("Failed to convert source to destination format "
                        "(%i/%i), please file a bug!",
                        (int)srcFormat, (int)dstFormat));
    return;
  }

  ffi::WGPUTexelCopyBufferLayout dataLayout = {0, &dstStrideVal, &dstHeight};
  ffi::WGPUTexelCopyTextureInfo copyView = {};
  CommandEncoder::ConvertTextureCopyViewToFFI(aDestination, &copyView);

  auto shmem_handle_index = GetChild()->QueueShmemHandle(std::move(handle));
  ffi::wgpu_queue_write_texture_via_shmem(GetClient(), mParent->GetId(),
                                          GetId(), copyView, dataLayout, extent,
                                          shmem_handle_index);
}

}  // namespace mozilla::webgpu
