dawn_node: Track promises
These should always be resolved or rejected.
The Fatal() call, when a promise is not resolved or rejected, is currently disabled due to https://github.com/gpuweb/cts/issues/784.
Bug: dawn:1123
Change-Id: Ie0e8ac187ad70be0fea41cd66956d0bfd9c53212
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/66821
Reviewed-by: Antonio Maiorano <amaiorano@google.com>
Commit-Queue: Ben Clayton <bclayton@google.com>
diff --git a/src/dawn_node/binding/GPU.cpp b/src/dawn_node/binding/GPU.cpp
index 2f64978..61e4d6f 100644
--- a/src/dawn_node/binding/GPU.cpp
+++ b/src/dawn_node/binding/GPU.cpp
@@ -55,8 +55,8 @@
interop::Promise<std::optional<interop::Interface<interop::GPUAdapter>>> GPU::requestAdapter(
Napi::Env env,
interop::GPURequestAdapterOptions options) {
- auto promise =
- interop::Promise<std::optional<interop::Interface<interop::GPUAdapter>>>(env);
+ auto promise = interop::Promise<std::optional<interop::Interface<interop::GPUAdapter>>>(
+ env, PROMISE_INFO);
if (options.forceFallbackAdapter) {
// Software adapters are not currently supported.
diff --git a/src/dawn_node/binding/GPUAdapter.cpp b/src/dawn_node/binding/GPUAdapter.cpp
index 82378d8..c49e442 100644
--- a/src/dawn_node/binding/GPUAdapter.cpp
+++ b/src/dawn_node/binding/GPUAdapter.cpp
@@ -139,7 +139,7 @@
Napi::Env env,
interop::GPUDeviceDescriptor descriptor) {
dawn_native::DeviceDescriptor desc{}; // TODO(crbug.com/dawn/1133): Fill in.
- interop::Promise<interop::Interface<interop::GPUDevice>> promise(env);
+ interop::Promise<interop::Interface<interop::GPUDevice>> promise(env, PROMISE_INFO);
// See src/dawn_native/Features.cpp for enum <-> string mappings.
for (auto required : descriptor.requiredFeatures) {
diff --git a/src/dawn_node/binding/GPUBuffer.cpp b/src/dawn_node/binding/GPUBuffer.cpp
index ac5ae54..2fca5d5 100644
--- a/src/dawn_node/binding/GPUBuffer.cpp
+++ b/src/dawn_node/binding/GPUBuffer.cpp
@@ -47,11 +47,13 @@
wgpu::MapMode md{};
Converter conv(env);
if (!conv(md, mode)) {
- return {env};
+ interop::Promise<void> promise(env, PROMISE_INFO);
+ promise.Reject(Errors::OperationError(env));
+ return promise;
}
if (state_ != State::Unmapped) {
- interop::Promise<void> promise(env);
+ interop::Promise<void> promise(env, PROMISE_INFO);
promise.Reject(Errors::OperationError(env));
device_.InjectError(wgpu::ErrorType::Validation,
"mapAsync called on buffer that is not in the unmapped state");
@@ -64,7 +66,7 @@
AsyncTask task;
State& state;
};
- auto ctx = new Context{env, interop::Promise<void>(env), async_, state_};
+ auto ctx = new Context{env, interop::Promise<void>(env, PROMISE_INFO), async_, state_};
auto promise = ctx->promise;
uint64_t s = size.has_value() ? size.value() : (desc_.size - offset);
diff --git a/src/dawn_node/binding/GPUDevice.cpp b/src/dawn_node/binding/GPUDevice.cpp
index 0870307..2f928e5 100644
--- a/src/dawn_node/binding/GPUDevice.cpp
+++ b/src/dawn_node/binding/GPUDevice.cpp
@@ -138,7 +138,12 @@
return interop::GPUQueue::Create<GPUQueue>(env, device_.GetQueue(), async_);
}
- void GPUDevice::destroy(Napi::Env) {
+ void GPUDevice::destroy(Napi::Env env) {
+ for (auto promise : lost_promises_) {
+ promise.Resolve(interop::GPUDeviceLostInfo::Create<DeviceLostInfo>(
+ env_, interop::GPUDeviceLostReason::kDestroyed, "device was destroyed"));
+ }
+ lost_promises_.clear();
device_.Release();
}
@@ -293,21 +298,23 @@
interop::Promise<interop::Interface<interop::GPUComputePipeline>>
GPUDevice::createComputePipelineAsync(Napi::Env env,
interop::GPUComputePipelineDescriptor descriptor) {
+ using Promise = interop::Promise<interop::Interface<interop::GPUComputePipeline>>;
+
Converter conv(env);
wgpu::ComputePipelineDescriptor desc{};
if (!conv(desc, descriptor)) {
- return {env};
+ Promise promise(env, PROMISE_INFO);
+ promise.Reject(Errors::OperationError(env));
+ return promise;
}
- using Promise = interop::Promise<interop::Interface<interop::GPUComputePipeline>>;
-
struct Context {
Napi::Env env;
Promise promise;
AsyncTask task;
};
- auto ctx = new Context{env, env, async_};
+ auto ctx = new Context{env, Promise(env, PROMISE_INFO), async_};
auto promise = ctx->promise;
device_.CreateComputePipelineAsync(
@@ -334,21 +341,23 @@
interop::Promise<interop::Interface<interop::GPURenderPipeline>>
GPUDevice::createRenderPipelineAsync(Napi::Env env,
interop::GPURenderPipelineDescriptor descriptor) {
+ using Promise = interop::Promise<interop::Interface<interop::GPURenderPipeline>>;
+
Converter conv(env);
wgpu::RenderPipelineDescriptor desc{};
if (!conv(desc, descriptor)) {
- return {env};
+ Promise promise(env, PROMISE_INFO);
+ promise.Reject(Errors::OperationError(env));
+ return promise;
}
- using Promise = interop::Promise<interop::Interface<interop::GPURenderPipeline>>;
-
struct Context {
Napi::Env env;
Promise promise;
AsyncTask task;
};
- auto ctx = new Context{env, env, async_};
+ auto ctx = new Context{env, Promise(env, PROMISE_INFO), async_};
auto promise = ctx->promise;
device_.CreateRenderPipelineAsync(
@@ -415,7 +424,8 @@
interop::Promise<interop::Interface<interop::GPUDeviceLostInfo>> GPUDevice::getLost(
Napi::Env env) {
- auto promise = interop::Promise<interop::Interface<interop::GPUDeviceLostInfo>>(env);
+ auto promise =
+ interop::Promise<interop::Interface<interop::GPUDeviceLostInfo>>(env, PROMISE_INFO);
lost_promises_.emplace_back(promise);
return promise;
}
@@ -444,7 +454,7 @@
Promise promise;
AsyncTask task;
};
- auto* ctx = new Context{env, env, async_};
+ auto* ctx = new Context{env, Promise(env, PROMISE_INFO), async_};
auto promise = ctx->promise;
bool ok = device_.PopErrorScope(
@@ -476,10 +486,8 @@
}
delete ctx;
- Promise p(env);
- p.Resolve(
- interop::GPUValidationError::Create<ValidationError>(env, "failed to pop error scope"));
- return p;
+ promise.Reject(Errors::OperationError(env));
+ return promise;
}
std::optional<std::string> GPUDevice::getLabel(Napi::Env) {
diff --git a/src/dawn_node/binding/GPUQueue.cpp b/src/dawn_node/binding/GPUQueue.cpp
index c8e39fe..e1c0413 100644
--- a/src/dawn_node/binding/GPUQueue.cpp
+++ b/src/dawn_node/binding/GPUQueue.cpp
@@ -51,7 +51,7 @@
interop::Promise<void> promise;
AsyncTask task;
};
- auto ctx = new Context{env, interop::Promise<void>(env), async_};
+ auto ctx = new Context{env, interop::Promise<void>(env, PROMISE_INFO), async_};
auto promise = ctx->promise;
queue_.OnSubmittedWorkDone(
diff --git a/src/dawn_node/binding/GPUShaderModule.cpp b/src/dawn_node/binding/GPUShaderModule.cpp
index 3323ba5..52efabd 100644
--- a/src/dawn_node/binding/GPUShaderModule.cpp
+++ b/src/dawn_node/binding/GPUShaderModule.cpp
@@ -91,7 +91,7 @@
Promise promise;
AsyncTask task;
};
- auto ctx = new Context{env, env, async_};
+ auto ctx = new Context{env, Promise(env, PROMISE_INFO), async_};
auto promise = ctx->promise;
shader_.GetCompilationInfo(
diff --git a/src/dawn_node/interop/Core.h b/src/dawn_node/interop/Core.h
index ff26930..dba1ad0 100644
--- a/src/dawn_node/interop/Core.h
+++ b/src/dawn_node/interop/Core.h
@@ -38,6 +38,13 @@
# define INTEROP_LOG(...)
#endif
+// A helper macro for constructing a PromiseInfo with the current file, function and line.
+// See PromiseInfo
+#define PROMISE_INFO \
+ ::wgpu::interop::PromiseInfo { \
+ __FILE__, __FUNCTION__, __LINE__ \
+ }
+
namespace wgpu { namespace interop {
////////////////////////////////////////////////////////////////////////////////
@@ -147,83 +154,106 @@
// Promise<T>
////////////////////////////////////////////////////////////////////////////////
+ // Info holds details about where the promise was constructed.
+ // Used for printing debug messages when a promise is finalized without being resolved
+ // or rejected.
+ // Use the PROMISE_INFO macro to populate this structure.
+ struct PromiseInfo {
+ const char* file = nullptr;
+ const char* function = nullptr;
+ int line = 0;
+ };
+
+ namespace detail {
+ // Base class for Promise<T> specializations.
+ class PromiseBase {
+ public:
+ // Implicit conversion operators to Napi promises.
+ inline operator napi_value() const {
+ return state->deferred.Promise();
+ }
+ inline operator Napi::Value() const {
+ return state->deferred.Promise();
+ }
+ inline operator Napi::Promise() const {
+ return state->deferred.Promise();
+ }
+
+ // Reject() rejects the promise with the given failure value.
+ void Reject(Napi::Value value) const {
+ state->deferred.Reject(value);
+ state->resolved_or_rejected = true;
+ }
+ void Reject(Napi::Error err) const {
+ Reject(err.Value());
+ }
+ void Reject(std::string err) const {
+ Reject(Napi::Error::New(state->deferred.Env(), err));
+ }
+
+ protected:
+ void Resolve(Napi::Value value) const {
+ state->deferred.Resolve(value);
+ state->resolved_or_rejected = true;
+ }
+
+ struct State {
+ Napi::Promise::Deferred deferred;
+ PromiseInfo info;
+ bool resolved_or_rejected = false;
+ };
+
+ PromiseBase(Napi::Env env, const PromiseInfo& info)
+ : state(new State{Napi::Promise::Deferred::New(env), info}) {
+ state->deferred.Promise().AddFinalizer(
+ [](Napi::Env, State* state) {
+ // TODO(https://github.com/gpuweb/cts/issues/784):
+ // Devices are never destroyed, so we always end up
+ // leaking the Device.lost promise. Enable this once
+ // fixed.
+ if ((false)) {
+ if (!state->resolved_or_rejected) {
+ ::wgpu::utils::Fatal("Promise not resolved or rejected",
+ state->info.file, state->info.line,
+ state->info.function);
+ }
+ }
+ delete state;
+ },
+ state);
+ }
+
+ State* const state;
+ };
+ } // namespace detail
+
// Promise<T> is a templated wrapper around a JavaScript promise, which can
// resolve to the template type T.
template <typename T>
- class Promise {
+ class Promise : public detail::PromiseBase {
public:
// Constructor
- Promise(Napi::Env env) : deferred(Napi::Promise::Deferred::New(env)) {
- }
-
- // Implicit conversion operators to Napi promises.
- inline operator napi_value() const {
- return deferred.Promise();
- }
- inline operator Napi::Value() const {
- return deferred.Promise();
- }
- inline operator Napi::Promise() const {
- return deferred.Promise();
+ Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) {
}
// Resolve() fulfills the promise with the given value.
void Resolve(T&& value) const {
- deferred.Resolve(ToJS(deferred.Env(), std::forward<T>(value)));
+ PromiseBase::Resolve(ToJS(state->deferred.Env(), std::forward<T>(value)));
}
-
- // Reject() rejects the promise with the given failure value.
- void Reject(Napi::Object obj) const {
- deferred.Reject(obj);
- }
- void Reject(Napi::Error err) const {
- deferred.Reject(err.Value());
- }
- void Reject(std::string err) const {
- Reject(Napi::Error::New(deferred.Env(), err));
- }
-
- private:
- Napi::Promise::Deferred deferred;
};
// Specialization for Promises that resolve with no value
template <>
- class Promise<void> {
+ class Promise<void> : public detail::PromiseBase {
public:
// Constructor
- Promise(Napi::Env env) : deferred(Napi::Promise::Deferred::New(env)) {
- }
-
- // Implicit conversion operators to Napi promises.
- inline operator napi_value() const {
- return deferred.Promise();
- }
- inline operator Napi::Value() const {
- return deferred.Promise();
- }
- inline operator Napi::Promise() const {
- return deferred.Promise();
+ Promise(Napi::Env env, const PromiseInfo& info) : PromiseBase(env, info) {
}
// Resolve() fulfills the promise.
void Resolve() const {
- deferred.Resolve(deferred.Env().Undefined());
+ PromiseBase::Resolve(state->deferred.Env().Undefined());
}
-
- // Reject() rejects the promise with the given failure value.
- void Reject(Napi::Object obj) const {
- deferred.Reject(obj);
- }
- void Reject(Napi::Error err) const {
- deferred.Reject(err.Value());
- }
- void Reject(std::string err) const {
- Reject(Napi::Error::New(deferred.Env(), err));
- }
-
- private:
- Napi::Promise::Deferred deferred;
};
////////////////////////////////////////////////////////////////////////////////