fune/dom/webgpu/Device.cpp

655 lines
22 KiB
C++

/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "js/ArrayBuffer.h"
#include "js/Value.h"
#include "mozilla/Attributes.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/Logging.h"
#include "mozilla/RefPtr.h"
#include "mozilla/dom/Promise.h"
#include "mozilla/dom/WebGPUBinding.h"
#include "Device.h"
#include "CommandEncoder.h"
#include "BindGroup.h"
#include "Adapter.h"
#include "Buffer.h"
#include "ComputePipeline.h"
#include "DeviceLostInfo.h"
#include "InternalError.h"
#include "OutOfMemoryError.h"
#include "PipelineLayout.h"
#include "Queue.h"
#include "RenderBundleEncoder.h"
#include "RenderPipeline.h"
#include "Sampler.h"
#include "SupportedFeatures.h"
#include "SupportedLimits.h"
#include "Texture.h"
#include "TextureView.h"
#include "ValidationError.h"
#include "ipc/WebGPUChild.h"
#include "Utility.h"
namespace mozilla::webgpu {
mozilla::LazyLogModule gWebGPULog("WebGPU");
GPU_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_INHERITED(Device, DOMEventTargetHelper,
mBridge, mQueue, mFeatures,
mLimits, mLostPromise);
NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(Device, DOMEventTargetHelper)
GPU_IMPL_JS_WRAP(Device)
/* static */ CheckedInt<uint32_t> Device::BufferStrideWithMask(
const gfx::IntSize& aSize, const gfx::SurfaceFormat& aFormat) {
constexpr uint32_t kBufferAlignmentMask = 0xff;
return CheckedInt<uint32_t>(aSize.width) * gfx::BytesPerPixel(aFormat) +
kBufferAlignmentMask;
}
RefPtr<WebGPUChild> Device::GetBridge() { return mBridge; }
Device::Device(Adapter* const aParent, RawId aId,
const ffi::WGPULimits& aRawLimits)
: DOMEventTargetHelper(aParent->GetParentObject()),
mId(aId),
// features are filled in Adapter::RequestDevice
mFeatures(new SupportedFeatures(aParent)),
mLimits(new SupportedLimits(aParent, aRawLimits)),
mBridge(aParent->mBridge),
mQueue(new class Queue(this, aParent->mBridge, aId)) {
mBridge->RegisterDevice(this);
}
Device::~Device() { Cleanup(); }
void Device::Cleanup() {
if (!mValid) {
return;
}
mValid = false;
if (mBridge) {
mBridge->UnregisterDevice(mId);
}
}
void Device::CleanupUnregisteredInParent() {
if (mBridge) {
mBridge->FreeUnregisteredInParentDevice(mId);
}
mValid = false;
}
bool Device::IsLost() const {
return !mBridge || !mBridge->CanSend() ||
(mLostPromise &&
(mLostPromise->State() != dom::Promise::PromiseState::Pending));
}
bool Device::IsBridgeAlive() const { return mBridge && mBridge->CanSend(); }
// Generate an error on the Device timeline for this device.
//
// aMessage is interpreted as UTF-8.
void Device::GenerateValidationError(const nsCString& aMessage) {
if (!IsBridgeAlive()) {
return; // Just drop it?
}
mBridge->SendGenerateError(Some(mId), dom::GPUErrorFilter::Validation,
aMessage);
}
void Device::TrackBuffer(Buffer* aBuffer) { mTrackedBuffers.Insert(aBuffer); }
void Device::UntrackBuffer(Buffer* aBuffer) { mTrackedBuffers.Remove(aBuffer); }
void Device::GetLabel(nsAString& aValue) const { aValue = mLabel; }
void Device::SetLabel(const nsAString& aLabel) { mLabel = aLabel; }
dom::Promise* Device::GetLost(ErrorResult& aRv) {
aRv = NS_OK;
if (!mLostPromise) {
mLostPromise = dom::Promise::Create(GetParentObject(), aRv);
if (mLostPromise && !mBridge->CanSend()) {
auto info = MakeRefPtr<DeviceLostInfo>(GetParentObject(),
u"WebGPUChild destroyed"_ns);
mLostPromise->MaybeResolve(info);
}
}
return mLostPromise;
}
void Device::ResolveLost(Maybe<dom::GPUDeviceLostReason> aReason,
const nsAString& aMessage) {
IgnoredErrorResult rv;
dom::Promise* lostPromise = GetLost(rv);
if (!lostPromise) {
// Promise doesn't exist? Maybe out of memory.
return;
}
if (lostPromise->State() != dom::Promise::PromiseState::Pending) {
// lostPromise was already resolved or rejected.
return;
}
if (!lostPromise->PromiseObj()) {
// The underlying JS object is gone.
return;
}
RefPtr<DeviceLostInfo> info;
if (aReason.isSome()) {
info = MakeRefPtr<DeviceLostInfo>(GetParentObject(), *aReason, aMessage);
} else {
info = MakeRefPtr<DeviceLostInfo>(GetParentObject(), aMessage);
}
lostPromise->MaybeResolve(info);
}
already_AddRefed<Buffer> Device::CreateBuffer(
const dom::GPUBufferDescriptor& aDesc, ErrorResult& aRv) {
return Buffer::Create(this, mId, aDesc, aRv);
}
already_AddRefed<Texture> Device::CreateTextureForSwapChain(
const dom::GPUCanvasConfiguration* const aConfig,
const gfx::IntSize& aCanvasSize, layers::RemoteTextureOwnerId aOwnerId) {
MOZ_ASSERT(aConfig);
dom::GPUTextureDescriptor desc;
desc.mDimension = dom::GPUTextureDimension::_2d;
auto& sizeDict = desc.mSize.SetAsGPUExtent3DDict();
sizeDict.mWidth = aCanvasSize.width;
sizeDict.mHeight = aCanvasSize.height;
sizeDict.mDepthOrArrayLayers = 1;
desc.mFormat = aConfig->mFormat;
desc.mMipLevelCount = 1;
desc.mSampleCount = 1;
desc.mUsage = aConfig->mUsage | dom::GPUTextureUsage_Binding::COPY_SRC;
desc.mViewFormats = aConfig->mViewFormats;
return CreateTexture(desc, Some(aOwnerId));
}
already_AddRefed<Texture> Device::CreateTexture(
const dom::GPUTextureDescriptor& aDesc) {
return CreateTexture(aDesc, /* aOwnerId */ Nothing());
}
already_AddRefed<Texture> Device::CreateTexture(
const dom::GPUTextureDescriptor& aDesc,
Maybe<layers::RemoteTextureOwnerId> aOwnerId) {
ffi::WGPUTextureDescriptor desc = {};
webgpu::StringHelper label(aDesc.mLabel);
desc.label = label.Get();
if (aDesc.mSize.IsRangeEnforcedUnsignedLongSequence()) {
const auto& seq = aDesc.mSize.GetAsRangeEnforcedUnsignedLongSequence();
desc.size.width = seq.Length() > 0 ? seq[0] : 1;
desc.size.height = seq.Length() > 1 ? seq[1] : 1;
desc.size.depth_or_array_layers = seq.Length() > 2 ? seq[2] : 1;
} else if (aDesc.mSize.IsGPUExtent3DDict()) {
const auto& dict = aDesc.mSize.GetAsGPUExtent3DDict();
desc.size.width = dict.mWidth;
desc.size.height = dict.mHeight;
desc.size.depth_or_array_layers = dict.mDepthOrArrayLayers;
} else {
MOZ_CRASH("Unexpected union");
}
desc.mip_level_count = aDesc.mMipLevelCount;
desc.sample_count = aDesc.mSampleCount;
desc.dimension = ffi::WGPUTextureDimension(aDesc.mDimension);
desc.format = ConvertTextureFormat(aDesc.mFormat);
desc.usage = aDesc.mUsage;
AutoTArray<ffi::WGPUTextureFormat, 8> viewFormats;
for (auto format : aDesc.mViewFormats) {
viewFormats.AppendElement(ConvertTextureFormat(format));
}
desc.view_formats = {viewFormats.Elements(), viewFormats.Length()};
Maybe<ffi::WGPUSwapChainId> ownerId;
if (aOwnerId.isSome()) {
ownerId = Some(ffi::WGPUSwapChainId{aOwnerId->mId});
}
ipc::ByteBuf bb;
RawId id = ffi::wgpu_client_create_texture(
mBridge->GetClient(), mId, &desc, ownerId.ptrOr(nullptr), ToFFI(&bb));
if (mBridge->CanSend()) {
mBridge->SendDeviceAction(mId, std::move(bb));
}
RefPtr<Texture> texture = new Texture(this, id, aDesc);
return texture.forget();
}
already_AddRefed<Sampler> Device::CreateSampler(
const dom::GPUSamplerDescriptor& aDesc) {
ffi::WGPUSamplerDescriptor desc = {};
webgpu::StringHelper label(aDesc.mLabel);
desc.label = label.Get();
desc.address_modes[0] = ffi::WGPUAddressMode(aDesc.mAddressModeU);
desc.address_modes[1] = ffi::WGPUAddressMode(aDesc.mAddressModeV);
desc.address_modes[2] = ffi::WGPUAddressMode(aDesc.mAddressModeW);
desc.mag_filter = ffi::WGPUFilterMode(aDesc.mMagFilter);
desc.min_filter = ffi::WGPUFilterMode(aDesc.mMinFilter);
desc.mipmap_filter = ffi::WGPUFilterMode(aDesc.mMipmapFilter);
desc.lod_min_clamp = aDesc.mLodMinClamp;
desc.lod_max_clamp = aDesc.mLodMaxClamp;
ffi::WGPUCompareFunction comparison = ffi::WGPUCompareFunction_Sentinel;
if (aDesc.mCompare.WasPassed()) {
comparison = ConvertCompareFunction(aDesc.mCompare.Value());
desc.compare = &comparison;
}
ipc::ByteBuf bb;
RawId id = ffi::wgpu_client_create_sampler(mBridge->GetClient(), mId, &desc,
ToFFI(&bb));
if (mBridge->CanSend()) {
mBridge->SendDeviceAction(mId, std::move(bb));
}
RefPtr<Sampler> sampler = new Sampler(this, id);
return sampler.forget();
}
already_AddRefed<CommandEncoder> Device::CreateCommandEncoder(
const dom::GPUCommandEncoderDescriptor& aDesc) {
RawId id = 0;
if (mBridge->CanSend()) {
id = mBridge->DeviceCreateCommandEncoder(mId, aDesc);
}
RefPtr<CommandEncoder> encoder = new CommandEncoder(this, mBridge, id);
return encoder.forget();
}
already_AddRefed<RenderBundleEncoder> Device::CreateRenderBundleEncoder(
const dom::GPURenderBundleEncoderDescriptor& aDesc) {
RefPtr<RenderBundleEncoder> encoder =
new RenderBundleEncoder(this, mBridge, aDesc);
return encoder.forget();
}
already_AddRefed<BindGroupLayout> Device::CreateBindGroupLayout(
const dom::GPUBindGroupLayoutDescriptor& aDesc) {
struct OptionalData {
ffi::WGPUTextureViewDimension dim;
ffi::WGPURawTextureSampleType type;
ffi::WGPUTextureFormat format;
};
nsTArray<OptionalData> optional(aDesc.mEntries.Length());
for (const auto& entry : aDesc.mEntries) {
OptionalData data = {};
if (entry.mTexture.WasPassed()) {
const auto& texture = entry.mTexture.Value();
data.dim = ffi::WGPUTextureViewDimension(texture.mViewDimension);
switch (texture.mSampleType) {
case dom::GPUTextureSampleType::Float:
data.type = ffi::WGPURawTextureSampleType_Float;
break;
case dom::GPUTextureSampleType::Unfilterable_float:
data.type = ffi::WGPURawTextureSampleType_UnfilterableFloat;
break;
case dom::GPUTextureSampleType::Uint:
data.type = ffi::WGPURawTextureSampleType_Uint;
break;
case dom::GPUTextureSampleType::Sint:
data.type = ffi::WGPURawTextureSampleType_Sint;
break;
case dom::GPUTextureSampleType::Depth:
data.type = ffi::WGPURawTextureSampleType_Depth;
break;
case dom::GPUTextureSampleType::EndGuard_:
MOZ_ASSERT_UNREACHABLE();
}
}
if (entry.mStorageTexture.WasPassed()) {
const auto& texture = entry.mStorageTexture.Value();
data.dim = ffi::WGPUTextureViewDimension(texture.mViewDimension);
data.format = ConvertTextureFormat(texture.mFormat);
}
optional.AppendElement(data);
}
nsTArray<ffi::WGPUBindGroupLayoutEntry> entries(aDesc.mEntries.Length());
for (size_t i = 0; i < aDesc.mEntries.Length(); ++i) {
const auto& entry = aDesc.mEntries[i];
ffi::WGPUBindGroupLayoutEntry e = {};
e.binding = entry.mBinding;
e.visibility = entry.mVisibility;
if (entry.mBuffer.WasPassed()) {
switch (entry.mBuffer.Value().mType) {
case dom::GPUBufferBindingType::Uniform:
e.ty = ffi::WGPURawBindingType_UniformBuffer;
break;
case dom::GPUBufferBindingType::Storage:
e.ty = ffi::WGPURawBindingType_StorageBuffer;
break;
case dom::GPUBufferBindingType::Read_only_storage:
e.ty = ffi::WGPURawBindingType_ReadonlyStorageBuffer;
break;
case dom::GPUBufferBindingType::EndGuard_:
MOZ_ASSERT_UNREACHABLE();
}
e.has_dynamic_offset = entry.mBuffer.Value().mHasDynamicOffset;
}
if (entry.mTexture.WasPassed()) {
e.ty = ffi::WGPURawBindingType_SampledTexture;
e.view_dimension = &optional[i].dim;
e.texture_sample_type = &optional[i].type;
e.multisampled = entry.mTexture.Value().mMultisampled;
}
if (entry.mStorageTexture.WasPassed()) {
e.ty = entry.mStorageTexture.Value().mAccess ==
dom::GPUStorageTextureAccess::Write_only
? ffi::WGPURawBindingType_WriteonlyStorageTexture
: ffi::WGPURawBindingType_ReadonlyStorageTexture;
e.view_dimension = &optional[i].dim;
e.storage_texture_format = &optional[i].format;
}
if (entry.mSampler.WasPassed()) {
e.ty = ffi::WGPURawBindingType_Sampler;
switch (entry.mSampler.Value().mType) {
case dom::GPUSamplerBindingType::Filtering:
e.sampler_filter = true;
break;
case dom::GPUSamplerBindingType::Non_filtering:
break;
case dom::GPUSamplerBindingType::Comparison:
e.sampler_compare = true;
break;
case dom::GPUSamplerBindingType::EndGuard_:
MOZ_ASSERT_UNREACHABLE();
}
}
entries.AppendElement(e);
}
ffi::WGPUBindGroupLayoutDescriptor desc = {};
webgpu::StringHelper label(aDesc.mLabel);
desc.label = label.Get();
desc.entries = entries.Elements();
desc.entries_length = entries.Length();
ipc::ByteBuf bb;
RawId id = ffi::wgpu_client_create_bind_group_layout(mBridge->GetClient(),
mId, &desc, ToFFI(&bb));
if (mBridge->CanSend()) {
mBridge->SendDeviceAction(mId, std::move(bb));
}
RefPtr<BindGroupLayout> object = new BindGroupLayout(this, id, true);
return object.forget();
}
already_AddRefed<PipelineLayout> Device::CreatePipelineLayout(
const dom::GPUPipelineLayoutDescriptor& aDesc) {
RawId id = 0;
if (mBridge->CanSend()) {
id = mBridge->DeviceCreatePipelineLayout(mId, aDesc);
}
RefPtr<PipelineLayout> object = new PipelineLayout(this, id);
return object.forget();
}
already_AddRefed<BindGroup> Device::CreateBindGroup(
const dom::GPUBindGroupDescriptor& aDesc) {
RawId id = 0;
if (mBridge->CanSend()) {
id = mBridge->DeviceCreateBindGroup(mId, aDesc);
}
RefPtr<BindGroup> object = new BindGroup(this, id);
return object.forget();
}
already_AddRefed<ShaderModule> Device::CreateShaderModule(
JSContext* aCx, const dom::GPUShaderModuleDescriptor& aDesc,
ErrorResult& aRv) {
Unused << aCx;
if (!mBridge->CanSend()) {
aRv.ThrowInvalidStateError("Connection to GPU process has shut down");
return nullptr;
}
RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
return MOZ_KnownLive(mBridge)->DeviceCreateShaderModule(this, aDesc, promise);
}
already_AddRefed<ComputePipeline> Device::CreateComputePipeline(
const dom::GPUComputePipelineDescriptor& aDesc) {
PipelineCreationContext context = {mId};
RawId id = 0;
if (mBridge->CanSend()) {
id = mBridge->DeviceCreateComputePipeline(&context, aDesc);
}
RefPtr<ComputePipeline> object =
new ComputePipeline(this, id, context.mImplicitPipelineLayoutId,
std::move(context.mImplicitBindGroupLayoutIds));
return object.forget();
}
already_AddRefed<RenderPipeline> Device::CreateRenderPipeline(
const dom::GPURenderPipelineDescriptor& aDesc) {
PipelineCreationContext context = {mId};
RawId id = 0;
if (mBridge->CanSend()) {
id = mBridge->DeviceCreateRenderPipeline(&context, aDesc);
}
RefPtr<RenderPipeline> object =
new RenderPipeline(this, id, context.mImplicitPipelineLayoutId,
std::move(context.mImplicitBindGroupLayoutIds));
return object.forget();
}
already_AddRefed<dom::Promise> Device::CreateComputePipelineAsync(
const dom::GPUComputePipelineDescriptor& aDesc, ErrorResult& aRv) {
RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
if (!mBridge->CanSend()) {
promise->MaybeRejectWithOperationError("Internal communication error");
return promise.forget();
}
std::shared_ptr<PipelineCreationContext> context(
new PipelineCreationContext());
context->mParentId = mId;
mBridge->DeviceCreateComputePipelineAsync(context.get(), aDesc)
->Then(
GetCurrentSerialEventTarget(), __func__,
[self = RefPtr{this}, context, promise](RawId aId) {
RefPtr<ComputePipeline> object = new ComputePipeline(
self, aId, context->mImplicitPipelineLayoutId,
std::move(context->mImplicitBindGroupLayoutIds));
promise->MaybeResolve(object);
},
[promise](const ipc::ResponseRejectReason&) {
promise->MaybeRejectWithOperationError(
"Internal communication error");
});
return promise.forget();
}
already_AddRefed<dom::Promise> Device::CreateRenderPipelineAsync(
const dom::GPURenderPipelineDescriptor& aDesc, ErrorResult& aRv) {
RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
if (!mBridge->CanSend()) {
promise->MaybeRejectWithOperationError("Internal communication error");
return promise.forget();
}
std::shared_ptr<PipelineCreationContext> context(
new PipelineCreationContext());
context->mParentId = mId;
mBridge->DeviceCreateRenderPipelineAsync(context.get(), aDesc)
->Then(
GetCurrentSerialEventTarget(), __func__,
[self = RefPtr{this}, context, promise](RawId aId) {
RefPtr<RenderPipeline> object = new RenderPipeline(
self, aId, context->mImplicitPipelineLayoutId,
std::move(context->mImplicitBindGroupLayoutIds));
promise->MaybeResolve(object);
},
[promise](const ipc::ResponseRejectReason&) {
promise->MaybeRejectWithOperationError(
"Internal communication error");
});
return promise.forget();
}
already_AddRefed<Texture> Device::InitSwapChain(
const dom::GPUCanvasConfiguration* const aConfig,
const layers::RemoteTextureOwnerId aOwnerId,
bool aUseExternalTextureInSwapChain, gfx::SurfaceFormat aFormat,
gfx::IntSize aCanvasSize) {
MOZ_ASSERT(aConfig);
if (!mBridge->CanSend()) {
return nullptr;
}
// Check that aCanvasSize and aFormat will generate a texture stride
// within limits.
const auto bufferStrideWithMask = BufferStrideWithMask(aCanvasSize, aFormat);
if (!bufferStrideWithMask.isValid()) {
return nullptr;
}
const layers::RGBDescriptor rgbDesc(aCanvasSize, aFormat);
// buffer count doesn't matter much, will be created on demand
const size_t maxBufferCount = 10;
mBridge->DeviceCreateSwapChain(mId, rgbDesc, maxBufferCount, aOwnerId,
aUseExternalTextureInSwapChain);
// TODO: `mColorSpace`: <https://bugzilla.mozilla.org/show_bug.cgi?id=1846608>
// TODO: `mAlphaMode`: <https://bugzilla.mozilla.org/show_bug.cgi?id=1846605>
return CreateTextureForSwapChain(aConfig, aCanvasSize, aOwnerId);
}
bool Device::CheckNewWarning(const nsACString& aMessage) {
return mKnownWarnings.EnsureInserted(aMessage);
}
void Device::Destroy() {
if (IsLost()) {
return;
}
// Unmap all buffers from this device, as specified by
// https://gpuweb.github.io/gpuweb/#dom-gpudevice-destroy.
dom::AutoJSAPI jsapi;
if (jsapi.Init(GetOwnerGlobal())) {
IgnoredErrorResult rv;
for (const auto& buffer : mTrackedBuffers) {
buffer->Unmap(jsapi.cx(), rv);
}
mTrackedBuffers.Clear();
}
mBridge->SendDeviceDestroy(mId);
}
void Device::PushErrorScope(const dom::GPUErrorFilter& aFilter) {
if (!IsBridgeAlive()) {
return;
}
mBridge->SendDevicePushErrorScope(mId, aFilter);
}
already_AddRefed<dom::Promise> Device::PopErrorScope(ErrorResult& aRv) {
/*
https://www.w3.org/TR/webgpu/#errors-and-debugging:
> After a device is lost (described below), errors are no longer surfaced.
> At this point, implementations do not need to run validation or error
tracking: > popErrorScope() and uncapturederror stop reporting errors, > and
the validity of objects on the device becomes unobservable.
*/
RefPtr<dom::Promise> promise = dom::Promise::Create(GetParentObject(), aRv);
if (NS_WARN_IF(aRv.Failed())) {
return nullptr;
}
if (!IsBridgeAlive()) {
WebGPUChild::JsWarning(
GetOwnerGlobal(),
"popErrorScope resolving to null because device is already lost."_ns);
promise->MaybeResolve(JS::NullHandleValue);
return promise.forget();
}
auto errorPromise = mBridge->SendDevicePopErrorScope(mId);
errorPromise->Then(
GetCurrentSerialEventTarget(), __func__,
[self = RefPtr{this}, promise](const PopErrorScopeResult& aResult) {
RefPtr<Error> error;
switch (aResult.resultType) {
case PopErrorScopeResultType::NoError:
promise->MaybeResolve(JS::NullHandleValue);
return;
case PopErrorScopeResultType::DeviceLost:
WebGPUChild::JsWarning(
self->GetOwnerGlobal(),
"popErrorScope resolving to null because device was lost."_ns);
promise->MaybeResolve(JS::NullHandleValue);
return;
case PopErrorScopeResultType::ThrowOperationError:
promise->MaybeRejectWithOperationError(aResult.message);
return;
case PopErrorScopeResultType::OutOfMemory:
error =
new OutOfMemoryError(self->GetParentObject(), aResult.message);
break;
case PopErrorScopeResultType::ValidationError:
error =
new ValidationError(self->GetParentObject(), aResult.message);
break;
case PopErrorScopeResultType::InternalError:
error = new InternalError(self->GetParentObject(), aResult.message);
break;
}
promise->MaybeResolve(std::move(error));
},
[self = RefPtr{this}, promise](const ipc::ResponseRejectReason&) {
// Device was lost.
WebGPUChild::JsWarning(
self->GetOwnerGlobal(),
"popErrorScope resolving to null because device was just lost."_ns);
promise->MaybeResolve(JS::NullHandleValue);
});
return promise.forget();
}
} // namespace mozilla::webgpu