Track OnSubmittedOnWorkDoneF futures using execution serials
This is needed on Vulkan because Vulkan cannot always make
system events from VkFences. Doing it on other backends
helps as well as an optimization. We don't need to create
a SystemEventPipe for *every* future, and instead create
them on demand when waiting is necessary.
Lazily creating system events then requires an extra level
of indirection to wrap them. `SystemEvent` does this
by storing an atomic boolean alongside an optional system
event sender/receiver pair.
Lastly, in order to avoid multiple vector allocations to convert
an array of TrackedFutureWaitInfo holding SystemEvent to arrays of
ready bools and SystemEventReceiver primitives which are passed to
the OS-specific waiting function, WaitAnySystemEvent is changed to
take a templated iterator so we can prepare the OS-specific handles
with fewer allocations.
This CL also removes the MustWaitUsingDevice concept from tracked
events since it will probably move to be a query on the device
instead of on each event. Mixed source waits are not supported yet,
and when they are, they will still want to reduce queue waits
to a single completion event. So, tracked events still need to
expose their queue/device instead of returning null when mixed
source waiting is enabled.
Fixed: dawn:2051
Fixed: dawn:2064
Bug: dawn:2150
Change-Id: I939d94c20b70c47013130d477029080c5240b804
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/156301
Reviewed-by: Loko Kung <lokokung@google.com>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Kokoro: Kokoro <noreply+kokoro@google.com>
Commit-Queue: Austin Eng <enga@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index 2f0f91f..4d8de67 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -378,6 +378,7 @@
"Toggles.h",
"UsageValidationMode.h",
"VisitableMembers.h",
+ "WaitAnySystemEvent.h",
"dawn_platform.h",
"stream/BlobSource.cpp",
"stream/BlobSource.h",
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 3b5f1d8..81c0ad6 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -230,6 +230,7 @@
"Toggles.h"
"UsageValidationMode.h"
"VisitableMembers.h"
+ "WaitAnySystemEvent.h"
"dawn_platform.h"
"webgpu_absl_format.cpp"
"webgpu_absl_format.h"
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index 786d13f..e50a938 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -2124,13 +2124,6 @@
return 4u;
}
-bool DeviceBase::WaitAnyImpl(size_t futureCount,
- TrackedFutureWaitInfo* futures,
- Nanoseconds timeout) {
- // Default for backends which don't actually need to do anything special in this case.
- return WaitAnySystemEvent(futureCount, futures, timeout);
-}
-
MaybeError DeviceBase::CopyFromStagingToBuffer(BufferBase* source,
uint64_t sourceOffset,
BufferBase* destination,
diff --git a/src/dawn/native/Device.h b/src/dawn/native/Device.h
index 9edb45e..fb1e83f 100644
--- a/src/dawn/native/Device.h
+++ b/src/dawn/native/Device.h
@@ -446,10 +446,6 @@
virtual void AppendDebugLayerMessages(ErrorData* error) {}
virtual void AppendDeviceLostMessage(ErrorData* error) {}
- [[nodiscard]] virtual bool WaitAnyImpl(size_t futureCount,
- TrackedFutureWaitInfo* futures,
- Nanoseconds timeout);
-
// It is guaranteed that the wrapped mutex will outlive the Device (if the Device is deleted
// before the AutoLockAndHoldRef).
[[nodiscard]] Mutex::AutoLockAndHoldRef GetScopedLockSafeForDelete();
diff --git a/src/dawn/native/EventManager.cpp b/src/dawn/native/EventManager.cpp
index a3099f8..26fd124 100644
--- a/src/dawn/native/EventManager.cpp
+++ b/src/dawn/native/EventManager.cpp
@@ -36,54 +36,192 @@
#include "dawn/common/FutureUtils.h"
#include "dawn/native/Device.h"
#include "dawn/native/IntegerTypes.h"
+#include "dawn/native/Queue.h"
#include "dawn/native/SystemEvent.h"
+#include "dawn/native/WaitAnySystemEvent.h"
namespace dawn::native {
namespace {
-// We can replace the std::vector& when std::span is available via C++20.
-wgpu::WaitStatus WaitImpl(std::vector<TrackedFutureWaitInfo>& futures, Nanoseconds timeout) {
- // Sort the futures by how they'll be waited (their GetWaitDevice).
- // This lets us do each wait on a slice of the array.
- std::sort(futures.begin(), futures.end(), [](const auto& a, const auto& b) {
- // operator<() is undefined behavior for arbitrary pointers, but std::less{}() is defined.
- return std::less<DeviceBase*>{}(a.event->GetWaitDevice(), b.event->GetWaitDevice());
- });
+// Wrapper around an iterator to yield system event receiver and a pointer
+// to the ready bool. We pass this into WaitAnySystemEvent so it can extract
+// the receivers and get pointers to the ready status - without allocating
+// duplicate storage to store the receivers and ready bools.
+class SystemEventAndReadyStateIterator {
+ public:
+ using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
- if (timeout > Nanoseconds(0)) {
- DAWN_ASSERT(futures.size() <= kTimedWaitAnyMaxCountDefault);
+ // Specify required iterator traits.
+ using value_type = std::pair<const SystemEventReceiver&, bool*>;
+ using difference_type = typename WrappedIter::difference_type;
+ using iterator_category = typename WrappedIter::iterator_category;
+ using pointer = value_type*;
+ using reference = value_type&;
- // If there's a timeout, check that there isn't a mix of wait devices.
- if (futures.front().event->GetWaitDevice() != futures.back().event->GetWaitDevice()) {
- return wgpu::WaitStatus::UnsupportedMixedSources;
- }
+ SystemEventAndReadyStateIterator() = default;
+ SystemEventAndReadyStateIterator(const SystemEventAndReadyStateIterator&) = default;
+ SystemEventAndReadyStateIterator& operator=(const SystemEventAndReadyStateIterator&) = default;
+
+ explicit SystemEventAndReadyStateIterator(WrappedIter wrappedIt) : mWrappedIt(wrappedIt) {}
+
+ bool operator!=(const SystemEventAndReadyStateIterator& rhs) {
+ return rhs.mWrappedIt != mWrappedIt;
+ }
+ bool operator==(const SystemEventAndReadyStateIterator& rhs) {
+ return rhs.mWrappedIt == mWrappedIt;
+ }
+ difference_type operator-(const SystemEventAndReadyStateIterator& rhs) {
+ return mWrappedIt - rhs.mWrappedIt;
}
- // Actually do the poll or wait to find out if any of the futures became ready.
- // Here, there's either only one iteration, or timeout is 0, so we know the
- // timeout won't get stacked multiple times.
- bool anySuccess = false;
- // Find each slice of the array (sliced by wait device), and wait on it.
- for (size_t sliceStart = 0; sliceStart < futures.size();) {
- DeviceBase* waitDevice = futures[sliceStart].event->GetWaitDevice();
- size_t sliceLength = 1;
- while (sliceStart + sliceLength < futures.size() &&
- (futures[sliceStart + sliceLength].event->GetWaitDevice()) == waitDevice) {
- sliceLength++;
- }
+ SystemEventAndReadyStateIterator& operator++() {
+ ++mWrappedIt;
+ return *this;
+ }
- {
- bool success;
- if (waitDevice) {
- success = waitDevice->WaitAnyImpl(sliceLength, &futures[sliceStart], timeout);
- } else {
- success = WaitAnySystemEvent(sliceLength, &futures[sliceStart], timeout);
+ value_type operator*() {
+ return {
+ std::get<Ref<SystemEvent>>(mWrappedIt->event->GetCompletionData())
+ ->GetOrCreateSystemEventReceiver(),
+ &mWrappedIt->ready,
+ };
+ }
+
+ private:
+ WrappedIter mWrappedIt;
+};
+
+// Wait/poll the queue for futures in range [begin, end). `waitSerial` should be
+// the serial after which at least one future should be complete. All futures must
+// have completion data of type QueueAndSerial.
+// Returns true if at least one future is ready. If no futures are ready or the wait
+// timed out, returns false.
+bool WaitQueueSerialsImpl(DeviceBase* device,
+ QueueBase* queue,
+ ExecutionSerial waitSerial,
+ std::vector<TrackedFutureWaitInfo>::iterator begin,
+ std::vector<TrackedFutureWaitInfo>::iterator end,
+ Nanoseconds timeout) {
+ bool success = false;
+ if (device->ConsumedError([&]() -> MaybeError {
+ if (waitSerial > queue->GetLastSubmittedCommandSerial()) {
+ // Serial has not been submitted yet. Submit it now.
+ // TODO(dawn:1413): This doesn't need to be a full tick. It just needs to
+ // flush work up to `waitSerial`. This should be done after the
+ // ExecutionQueue / ExecutionContext refactor.
+ queue->ForceEventualFlushOfCommands();
+ DAWN_TRY(device->Tick());
}
- anySuccess |= success;
+ // Check the completed serial.
+ ExecutionSerial completedSerial = queue->GetCompletedCommandSerial();
+ if (completedSerial < waitSerial) {
+ if (timeout > Nanoseconds(0)) {
+ // Wait on the serial if it hasn't passed yet.
+ DAWN_TRY_ASSIGN(success, queue->WaitForQueueSerial(waitSerial, timeout));
+ }
+ // Update completed serials.
+ DAWN_TRY(queue->CheckPassedSerials());
+ completedSerial = queue->GetCompletedCommandSerial();
+ }
+ // Poll futures for completion.
+ for (auto it = begin; it != end; ++it) {
+ ExecutionSerial serial =
+ std::get<QueueAndSerial>(it->event->GetCompletionData()).completionSerial;
+ if (serial <= completedSerial) {
+ success = true;
+ it->ready = true;
+ }
+ }
+ return {};
+ }())) {
+ // There was an error. Pending submit may have failed or waiting for fences
+ // may have lost the device. The device is lost inside ConsumedError.
+ // Mark all futures as ready.
+ for (auto it = begin; it != end; ++it) {
+ it->ready = true;
+ }
+ success = true;
+ }
+ return success;
+}
+
+// We can replace the std::vector& when std::span is available via C++20.
+wgpu::WaitStatus WaitImpl(std::vector<TrackedFutureWaitInfo>& futures, Nanoseconds timeout) {
+ auto begin = futures.begin();
+ const auto end = futures.end();
+ bool anySuccess = false;
+ // The following loop will partition [begin, end) based on the type of wait is required.
+ // After each partition, it will wait/poll on the first partition, then advance `begin`
+ // to the start of the next partition. Note that for timeout > 0 and unsupported mixed
+ // sources, we validate that there is a single partition. If there is only one, then the
+ // loop runs only once and the timeout does not stack.
+ while (begin != end) {
+ const auto& first = begin->event->GetCompletionData();
+
+ DeviceBase* waitDevice;
+ ExecutionSerial lowestWaitSerial;
+ if (std::holds_alternative<Ref<SystemEvent>>(first)) {
+ waitDevice = nullptr;
+ } else {
+ const auto& queueAndSerial = std::get<QueueAndSerial>(first);
+ waitDevice = queueAndSerial.queue->GetDevice();
+ lowestWaitSerial = queueAndSerial.completionSerial;
+ }
+ // Partition the remaining futures based on whether they match the same completion
+ // data type as the first. Also keep track of the lowest wait serial.
+ const auto mid =
+ std::partition(std::next(begin), end, [&](const TrackedFutureWaitInfo& info) {
+ const auto& completionData = info.event->GetCompletionData();
+ if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
+ return waitDevice == nullptr;
+ } else {
+ const auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
+ if (waitDevice == queueAndSerial.queue->GetDevice()) {
+ lowestWaitSerial =
+ std::min(lowestWaitSerial, queueAndSerial.completionSerial);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ });
+
+ // There's a mix of wait sources if partition yielded an iterator that is not at the end.
+ if (mid != end) {
+ if (timeout > Nanoseconds(0)) {
+ // Multi-source wait is unsupported.
+ // TODO(dawn:2062): Implement support for this when the device supports it.
+ // It should eventually gather the lowest serial from the queue(s), transform them
+ // into completion events, and wait on all of the events. Then for any queues that
+ // saw a completion, poll all futures related to that queue for completion.
+ return wgpu::WaitStatus::UnsupportedMixedSources;
+ }
}
- sliceStart += sliceLength;
+ bool success;
+ if (waitDevice) {
+ success = WaitQueueSerialsImpl(waitDevice, std::get<QueueAndSerial>(first).queue.Get(),
+ lowestWaitSerial, begin, mid, timeout);
+ } else {
+ if (timeout > Nanoseconds(0)) {
+ success = WaitAnySystemEvent(SystemEventAndReadyStateIterator{begin},
+ SystemEventAndReadyStateIterator{mid}, timeout);
+ } else {
+ // Poll the completion events.
+ success = false;
+ for (auto it = begin; it != end; ++it) {
+ if (std::get<Ref<SystemEvent>>(it->event->GetCompletionData())->IsSignaled()) {
+ it->ready = true;
+ success = true;
+ }
+ }
+ }
+ }
+ anySuccess |= success;
+
+ // Advance the iterator to the next partition.
+ begin = mid;
}
if (!anySuccess) {
return wgpu::WaitStatus::TimedOut;
@@ -262,21 +400,22 @@
// EventManager::TrackedEvent
-EventManager::TrackedEvent::TrackedEvent(DeviceBase* device,
- wgpu::CallbackMode callbackMode,
- SystemEventReceiver&& receiver)
- : mDevice(device), mCallbackMode(callbackMode), mReceiver(std::move(receiver)) {}
+EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
+ Ref<SystemEvent> completionEvent)
+ : mCallbackMode(callbackMode), mCompletionData(std::move(completionEvent)) {}
+
+EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
+ QueueBase* queue,
+ ExecutionSerial completionSerial)
+ : mCallbackMode(callbackMode), mCompletionData(QueueAndSerial{queue, completionSerial}) {}
EventManager::TrackedEvent::~TrackedEvent() {
DAWN_ASSERT(mCompleted);
}
-const SystemEventReceiver& EventManager::TrackedEvent::GetReceiver() const {
- return mReceiver;
-}
-
-DeviceBase* EventManager::TrackedEvent::GetWaitDevice() const {
- return MustWaitUsingDevice() ? mDevice.Get() : nullptr;
+const EventManager::TrackedEvent::CompletionData& EventManager::TrackedEvent::GetCompletionData()
+ const {
+ return mCompletionData;
}
void EventManager::TrackedEvent::EnsureComplete(EventCompletionType completionType) {
diff --git a/src/dawn/native/EventManager.h b/src/dawn/native/EventManager.h
index 1c39974..0bdf0f2 100644
--- a/src/dawn/native/EventManager.h
+++ b/src/dawn/native/EventManager.h
@@ -33,7 +33,7 @@
#include <mutex>
#include <optional>
#include <unordered_map>
-#include <vector>
+#include <variant>
#include "dawn/common/FutureUtils.h"
#include "dawn/common/MutexProtected.h"
@@ -54,13 +54,10 @@
// TODO(crbug.com/dawn/2050): Can this eventually replace CallbackTaskManager?
//
// There are various ways to optimize ProcessEvents/WaitAny:
-// - TODO(crbug.com/dawn/2064) Only pay attention to the earliest serial on each queue.
// - TODO(crbug.com/dawn/2059) Spontaneously set events as "early-ready" in other places when we see
// serials advance, e.g. Submit, or when checking a later wait before an earlier wait.
// - TODO(crbug.com/dawn/2049) For thread-driven events (async pipeline compilation and Metal queue
// events), defer tracking for ProcessEvents until the event is already completed.
-// - TODO(crbug.com/dawn/2051) Avoid creating OS events until they're actually needed (see the todo
-// in TrackedEvent).
class EventManager final : NonMovable {
public:
EventManager();
@@ -94,6 +91,11 @@
std::optional<MutexProtected<EventMap>> mEvents;
};
+struct QueueAndSerial {
+ Ref<QueueBase> queue;
+ ExecutionSerial completionSerial;
+};
+
// Base class for the objects that back WGPUFutures. TrackedEvent is responsible for the lifetime
// the callback it contains. If TrackedEvent gets destroyed before it completes, it's responsible
// for cleaning up (by calling the callback with an "Unknown" status).
@@ -107,9 +109,12 @@
protected:
// Note: TrackedEvents are (currently) only for Device events. Events like RequestAdapter and
// RequestDevice complete immediately in dawn native, so should never need to be tracked.
- TrackedEvent(DeviceBase* device,
- wgpu::CallbackMode callbackMode,
- SystemEventReceiver&& receiver);
+ TrackedEvent(wgpu::CallbackMode callbackMode, Ref<SystemEvent> completionEvent);
+
+ // Create a TrackedEvent from a queue completion serial.
+ TrackedEvent(wgpu::CallbackMode callbackMode,
+ QueueBase* queue,
+ ExecutionSerial completionSerial);
public:
// Subclasses must implement this to complete the event (if not completed) with
@@ -117,23 +122,27 @@
~TrackedEvent() override;
class WaitRef;
+ // Events may be one of two types:
+ // - A queue and the ExecutionSerial after which the event will be completed.
+ // Used for queue completion.
+ // - A SystemEvent which will be signaled from our code, usually on a separate thread.
+ // It stores a boolean that we can check instead of polling with the OS, or it can be
+ // transformed lazily into a SystemEventReceiver. Used for async pipeline creation, and Metal
+ // queue completion.
+ // The queue ref creates a temporary ref cycle
+ // (Queue->Device->Instance->EventManager->TrackedEvent). This is OK because the instance will
+ // clear out the EventManager on shutdown.
+ // TODO(crbug.com/dawn/2067): This is a bit fragile. Is it possible to remove the ref cycle?
+ using CompletionData = std::variant<QueueAndSerial, Ref<SystemEvent>>;
- const SystemEventReceiver& GetReceiver() const;
- DeviceBase* GetWaitDevice() const;
+ const CompletionData& GetCompletionData() const;
protected:
void EnsureComplete(EventCompletionType);
void CompleteIfSpontaneous();
- // True if the event can only be waited using its device (e.g. with vkWaitForFences).
- // False if it can be waited using OS-level wait primitives (WaitAnySystemEvent).
- virtual bool MustWaitUsingDevice() const = 0;
virtual void Complete(EventCompletionType) = 0;
- // This creates a temporary ref cycle (Device->Instance->EventManager->TrackedEvent).
- // This is OK because the instance will clear out the EventManager on shutdown.
- // TODO(crbug.com/dawn/2067): This is a bit fragile. Is it possible to remove the ref cycle?
- Ref<DeviceBase> mDevice;
wgpu::CallbackMode mCallbackMode;
#if DAWN_ENABLE_ASSERTS
@@ -143,20 +152,7 @@
private:
friend class EventManager;
- // TODO(crbug.com/dawn/2051): Optimize by creating an SystemEventReceiver only once actually
- // needed (the user asks for a timed wait or an OS event handle). This should be generally
- // achievable:
- // - For thread-driven events (async pipeline compilation and Metal queue events), use a mutex
- // or atomics to atomically:
- // - On wait: { check if mKnownReady. if not, create the SystemEventPipe }
- // - On signal: { check if there's an SystemEventPipe. if not, set mKnownReady }
- // - For D3D12/Vulkan fences, on timed waits, first use GetCompletedValue/GetFenceStatus, then
- // create an OS event if it's not ready yet (and we don't have one yet).
- //
- // This abstraction should probably be hidden from TrackedEvent - previous attempts to do
- // something similar in TrackedEvent turned out to be quite confusing. It can instead be an
- // "optimization" to the SystemEvent* or a layer between TrackedEvent and SystemEventReceiver.
- SystemEventReceiver mReceiver;
+ CompletionData mCompletionData;
// Callback has been called.
std::atomic<bool> mCompleted = false;
};
diff --git a/src/dawn/native/ExecutionQueue.cpp b/src/dawn/native/ExecutionQueue.cpp
index f8c3c8e..87de382 100644
--- a/src/dawn/native/ExecutionQueue.cpp
+++ b/src/dawn/native/ExecutionQueue.cpp
@@ -74,6 +74,17 @@
HasPendingCommands();
}
+Ref<SystemEvent> ExecutionQueueBase::CreateWorkDoneSystemEvent(ExecutionSerial serial) {
+ // TODO(crbug.com/dawn/2058): Implement this in all backends and remove this default impl
+ DAWN_CHECK(false);
+}
+
+ResultOrError<bool> ExecutionQueueBase::WaitForQueueSerial(ExecutionSerial serial,
+ Nanoseconds timeout) {
+ // TODO(crbug.com/dawn/2058): Implement this in all backends and remove this default impl
+ return DAWN_UNIMPLEMENTED_ERROR("Not implemented");
+}
+
// All prevously submitted works at the moment will supposedly complete at this serial.
// Internally the serial is computed according to whether frontend and backend have pending
// commands. There are 4 cases of combination:
diff --git a/src/dawn/native/ExecutionQueue.h b/src/dawn/native/ExecutionQueue.h
index 52eeeae..d6569b0 100644
--- a/src/dawn/native/ExecutionQueue.h
+++ b/src/dawn/native/ExecutionQueue.h
@@ -31,6 +31,7 @@
#include <atomic>
#include "dawn/native/Error.h"
+#include "dawn/native/EventManager.h"
#include "dawn/native/IntegerTypes.h"
namespace dawn::native {
@@ -73,6 +74,12 @@
// resources.
virtual MaybeError WaitForIdleForDestruction() = 0;
+ // Get or create an event that will be complete after the ExecutionSerial passes.
+ virtual Ref<SystemEvent> CreateWorkDoneSystemEvent(ExecutionSerial serial);
+ // Wait at most `timeout` synchronously for the ExecutionSerial to pass. Returns true
+ // if the serial passed.
+ virtual ResultOrError<bool> WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout);
+
// In the 'Normal' mode, currently recorded commands in the backend submitted in the next Tick.
// However in the 'Passive' mode, the submission will be postponed as late as possible, for
// example, until the client has explictly issued a submission.
diff --git a/src/dawn/native/Queue.cpp b/src/dawn/native/Queue.cpp
index 16e25a9..b17ebfd 100644
--- a/src/dawn/native/Queue.cpp
+++ b/src/dawn/native/Queue.cpp
@@ -196,19 +196,19 @@
WGPUQueueWorkDoneCallback mCallback;
void* mUserdata;
- // Create an event backed by the given SystemEventReceiver.
- WorkDoneEvent(DeviceBase* device,
- const QueueWorkDoneCallbackInfo& callbackInfo,
- SystemEventReceiver&& receiver)
- : TrackedEvent(device, callbackInfo.mode, std::move(receiver)),
+ // Create an event backed by the given queue execution serial.
+ WorkDoneEvent(const QueueWorkDoneCallbackInfo& callbackInfo,
+ QueueBase* queue,
+ ExecutionSerial serial)
+ : TrackedEvent(callbackInfo.mode, queue, serial),
mCallback(callbackInfo.callback),
mUserdata(callbackInfo.userdata) {}
// Create an event that's ready at creation (for errors, etc.)
- WorkDoneEvent(DeviceBase* device,
- const QueueWorkDoneCallbackInfo& callbackInfo,
+ WorkDoneEvent(const QueueWorkDoneCallbackInfo& callbackInfo,
+ QueueBase* queue,
wgpu::QueueWorkDoneStatus earlyStatus)
- : TrackedEvent(device, callbackInfo.mode, SystemEventReceiver::CreateAlreadySignaled()),
+ : TrackedEvent(callbackInfo.mode, queue, kBeginningOfGPUTime),
mEarlyStatus(earlyStatus),
mCallback(callbackInfo.callback),
mUserdata(callbackInfo.userdata) {
@@ -217,10 +217,6 @@
~WorkDoneEvent() override { EnsureComplete(EventCompletionType::Shutdown); }
- // TODO(crbug.com/dawn/2062): When adding support for mixed sources, return false here when
- // the device has the mixed sources feature enabled, and so can expose the fence as an OS event.
- bool MustWaitUsingDevice() const override { return true; }
-
void Complete(EventCompletionType completionType) override {
// WorkDoneEvent has no error cases other than the mEarlyStatus ones.
wgpu::QueueWorkDoneStatus status = wgpu::QueueWorkDoneStatus::Success;
@@ -319,9 +315,9 @@
}
// Note: if the callback is spontaneous, it'll get called in here.
- event = AcquireRef(new WorkDoneEvent(GetDevice(), callbackInfo, validationEarlyStatus));
+ event = AcquireRef(new WorkDoneEvent(callbackInfo, this, validationEarlyStatus));
} else {
- event = AcquireRef(new WorkDoneEvent(GetDevice(), callbackInfo, InsertWorkDoneEvent()));
+ event = AcquireRef(new WorkDoneEvent(callbackInfo, this, GetScheduledWorkDoneSerial()));
}
FutureID futureID =
@@ -330,11 +326,6 @@
return {futureID};
}
-SystemEventReceiver QueueBase::InsertWorkDoneEvent() {
- // TODO(crbug.com/dawn/2058): Implement this in all backends and remove this default impl
- DAWN_CHECK(false);
-}
-
void QueueBase::TrackTask(std::unique_ptr<TrackTaskCallback> task, ExecutionSerial serial) {
// If the task depends on a serial which is not submitted yet, force a flush.
if (serial > GetLastSubmittedCommandSerial()) {
diff --git a/src/dawn/native/Queue.h b/src/dawn/native/Queue.h
index 0cdf3f4..c802739 100644
--- a/src/dawn/native/Queue.h
+++ b/src/dawn/native/Queue.h
@@ -112,7 +112,6 @@
QueueBase(DeviceBase* device, ObjectBase::ErrorTag tag, const char* label);
void DestroyImpl() override;
- virtual SystemEventReceiver InsertWorkDoneEvent();
private:
MaybeError WriteTextureInternal(const ImageCopyTexture* destination,
diff --git a/src/dawn/native/SystemEvent.cpp b/src/dawn/native/SystemEvent.cpp
index 44ac664..c330870 100644
--- a/src/dawn/native/SystemEvent.cpp
+++ b/src/dawn/native/SystemEvent.cpp
@@ -44,29 +44,6 @@
namespace dawn::native {
-namespace {
-
-template <typename T, T Infinity>
-T ToMillisecondsGeneric(Nanoseconds timeout) {
- uint64_t ns = uint64_t{timeout};
- uint64_t ms = 0;
- if (ns > 0) {
- ms = (ns - 1) / 1'000'000 + 1;
- if (ms > std::numeric_limits<T>::max()) {
- return Infinity; // Round long timeout up to infinity
- }
- }
- return static_cast<T>(ms);
-}
-
-#if DAWN_PLATFORM_IS(WINDOWS)
-// #define ToMilliseconds ToMillisecondsGeneric<DWORD, INFINITE>
-#elif DAWN_PLATFORM_IS(POSIX)
-#define ToMilliseconds ToMillisecondsGeneric<int, -1>
-#endif
-
-} // namespace
-
// SystemEventReceiver
SystemEventReceiver SystemEventReceiver::CreateAlreadySignaled() {
@@ -85,6 +62,10 @@
DAWN_ASSERT(!mPrimitive.IsValid());
}
+bool SystemEventPipeSender::IsValid() const {
+ return mPrimitive.IsValid();
+}
+
void SystemEventPipeSender::Signal() && {
DAWN_ASSERT(mPrimitive.IsValid());
#if DAWN_PLATFORM_IS(WINDOWS)
@@ -103,43 +84,6 @@
mPrimitive.Close();
}
-// standalone functions
-
-bool WaitAnySystemEvent(size_t count, TrackedFutureWaitInfo* futures, Nanoseconds timeout) {
-#if DAWN_PLATFORM_IS(WINDOWS)
- // TODO(crbug.com/dawn/2054): Implement this.
- DAWN_CHECK(false);
-#elif DAWN_PLATFORM_IS(POSIX)
- std::vector<pollfd> pollfds(count);
- for (size_t i = 0; i < count; ++i) {
- int fd = futures[i].event->GetReceiver().mPrimitive.Get();
- pollfds[i] = pollfd{fd, POLLIN, 0};
- }
-
- int status = poll(pollfds.data(), pollfds.size(), ToMilliseconds(timeout));
-
- DAWN_CHECK(status >= 0);
- if (status == 0) {
- return false;
- }
-
- for (size_t i = 0; i < count; ++i) {
- int revents = pollfds[i].revents;
- static constexpr int kAllowedEvents = POLLIN | POLLHUP;
- DAWN_CHECK((revents & kAllowedEvents) == revents);
- }
-
- for (size_t i = 0; i < count; ++i) {
- bool ready = (pollfds[i].revents & POLLIN) != 0;
- futures[i].ready = ready;
- }
-
- return true;
-#else
- DAWN_CHECK(false); // Not implemented.
-#endif
-}
-
std::pair<SystemEventPipeSender, SystemEventReceiver> CreateSystemEventPipe() {
#if DAWN_PLATFORM_IS(WINDOWS)
// This is not needed on Windows yet. It's implementable using CreateEvent().
@@ -162,4 +106,40 @@
#endif
}
+// SystemEvent
+
+bool SystemEvent::IsSignaled() const {
+ return mSignaled.load(std::memory_order_acquire);
+}
+
+void SystemEvent::Signal() {
+ if (!mSignaled.exchange(true, std::memory_order_acq_rel)) {
+ mPipe.Use([](auto pipe) {
+ // Check if there is a pipe and the sender is valid.
+ // This function may race with GetOrCreateSystemEventReceiver such that the pipe is
+ // already signaled and the sender is invalid.
+ if (*pipe && pipe->value().first.IsValid()) {
+ std::move(pipe->value().first).Signal();
+ }
+ });
+ }
+}
+
+const SystemEventReceiver& SystemEvent::GetOrCreateSystemEventReceiver() {
+ return mPipe.Use([this](auto pipe) {
+ if (!*pipe) {
+ // Check whether the event was marked as completed. This may have happened if
+ // this function races with another thread performing Signal. If we won
+ // the race, then the pipe we just created will get signaled inside Signal.
+ // If we lost the race, then it will not be signaled and we must do it now.
+ if (IsSignaled()) {
+ *pipe = {SystemEventPipeSender{}, SystemEventReceiver::CreateAlreadySignaled()};
+ } else {
+ *pipe = CreateSystemEventPipe();
+ }
+ }
+ return std::cref(pipe->value().second);
+ });
+}
+
} // namespace dawn::native
diff --git a/src/dawn/native/SystemEvent.h b/src/dawn/native/SystemEvent.h
index cbd9be9..34c24d0 100644
--- a/src/dawn/native/SystemEvent.h
+++ b/src/dawn/native/SystemEvent.h
@@ -28,10 +28,13 @@
#ifndef SRC_DAWN_NATIVE_SYSTEMEVENT_H_
#define SRC_DAWN_NATIVE_SYSTEMEVENT_H_
+#include <optional>
#include <utility>
+#include "dawn/common/MutexProtected.h"
#include "dawn/common/NonCopyable.h"
#include "dawn/common/Platform.h"
+#include "dawn/common/RefCounted.h"
#include "dawn/native/IntegerTypes.h"
#include "dawn/native/SystemHandle.h"
@@ -57,7 +60,8 @@
SystemEventReceiver& operator=(SystemEventReceiver&&) = default;
private:
- friend bool WaitAnySystemEvent(size_t, TrackedFutureWaitInfo*, Nanoseconds);
+ template <typename It>
+ friend bool WaitAnySystemEvent(It begin, It end, Nanoseconds timeout);
friend std::pair<SystemEventPipeSender, SystemEventReceiver> CreateSystemEventPipe();
SystemHandle mPrimitive;
};
@@ -70,6 +74,7 @@
SystemEventPipeSender& operator=(SystemEventPipeSender&&) = default;
~SystemEventPipeSender();
+ bool IsValid() const;
void Signal() &&;
private:
@@ -77,12 +82,6 @@
SystemHandle mPrimitive;
};
-// Implementation of WaitAny when backed by SystemEventReceiver.
-// Returns true if some future is now ready, false if not (it timed out).
-[[nodiscard]] bool WaitAnySystemEvent(size_t count,
- TrackedFutureWaitInfo* futures,
- Nanoseconds timeout);
-
// CreateSystemEventPipe provides an SystemEventReceiver that can be signalled by Dawn code. This is
// useful for queue completions on Metal (where Metal signals us by calling a callback) and for
// async pipeline creations that happen in a worker-thread task.
@@ -102,6 +101,23 @@
// signal it by write()ing into the pipe (to make it become readable, though we won't read() it).
std::pair<SystemEventPipeSender, SystemEventReceiver> CreateSystemEventPipe();
+class SystemEvent : public RefCounted {
+ public:
+ bool IsSignaled() const;
+ void Signal();
+
+ // Lazily create a system event receiver. Immediately after this receiver
+ // is signaled, IsSignaled should always return true.
+ const SystemEventReceiver& GetOrCreateSystemEventReceiver();
+
+ private:
+ // mSignaled indicates whether the event has already been signaled.
+ // It is stored outside the mPipe mutex so its status can quickly be checked without
+ // acquiring a lock.
+ std::atomic<bool> mSignaled{false};
+ MutexProtected<std::optional<std::pair<SystemEventPipeSender, SystemEventReceiver>>> mPipe;
+};
+
} // namespace dawn::native
#endif // SRC_DAWN_NATIVE_SYSTEMEVENT_H_
diff --git a/src/dawn/native/WaitAnySystemEvent.h b/src/dawn/native/WaitAnySystemEvent.h
new file mode 100644
index 0000000..be2ebe4
--- /dev/null
+++ b/src/dawn/native/WaitAnySystemEvent.h
@@ -0,0 +1,111 @@
+// Copyright 2023 The Dawn & Tint Authors
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are met:
+//
+// 1. Redistributions of source code must retain the above copyright notice, this
+// list of conditions and the following disclaimer.
+//
+// 2. Redistributions in binary form must reproduce the above copyright notice,
+// this list of conditions and the following disclaimer in the documentation
+// and/or other materials provided with the distribution.
+//
+// 3. Neither the name of the copyright holder nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef SRC_DAWN_NATIVE_WAITANYSYSTEMEVENT_H_
+#define SRC_DAWN_NATIVE_WAITANYSYSTEMEVENT_H_
+
+#include <limits>
+#include <utility>
+
+#include "dawn/common/Platform.h"
+
+#if DAWN_PLATFORM_IS(WINDOWS)
+#include "dawn/common/windows_with_undefs.h"
+#elif DAWN_PLATFORM_IS(POSIX)
+#include <sys/poll.h>
+#include <unistd.h>
+#endif
+
+#include "dawn/common/StackContainer.h"
+#include "dawn/native/SystemEvent.h"
+
+namespace dawn::native {
+
+template <typename T, T Infinity>
+T ToMillisecondsGeneric(Nanoseconds timeout) {
+ uint64_t ns = uint64_t{timeout};
+ uint64_t ms = 0;
+ if (ns > 0) {
+ ms = (ns - 1) / 1'000'000 + 1;
+ if (ms > std::numeric_limits<T>::max()) {
+ return Infinity; // Round long timeout up to infinity
+ }
+ }
+ return static_cast<T>(ms);
+}
+
+#if DAWN_PLATFORM_IS(WINDOWS)
+// #define ToMilliseconds ToMillisecondsGeneric<DWORD, INFINITE>
+#elif DAWN_PLATFORM_IS(POSIX)
+#define ToMilliseconds ToMillisecondsGeneric<int, -1>
+#endif
+
+// WaitAnySystemEvent on an iterator range converts those iterators to the
+// platform-specific wait handles, and then waits on them.
+template <typename It>
+[[nodiscard]] bool WaitAnySystemEvent(It begin, It end, Nanoseconds timeout) {
+ static_assert(std::is_same_v<typename std::iterator_traits<It>::value_type,
+ std::pair<const SystemEventReceiver&, bool*>>);
+ size_t count = std::distance(begin, end);
+ if (count == 0) {
+ return false;
+ }
+#if DAWN_PLATFORM_IS(WINDOWS)
+ // TODO(crbug.com/dawn/2054): Implement this.
+ DAWN_CHECK(false);
+#elif DAWN_PLATFORM_IS(POSIX)
+ StackVector<pollfd, 4 /* avoid heap allocation for small waits */> pollfds;
+ pollfds->reserve(count);
+ for (auto it = begin; it != end; ++it) {
+ pollfds->push_back(pollfd{(*it).first.mPrimitive.Get(), POLLIN, 0});
+ }
+ int status = poll(pollfds->data(), pollfds->size(), ToMilliseconds(timeout));
+
+ DAWN_CHECK(status >= 0);
+ if (status == 0) {
+ return false;
+ }
+
+ size_t i = 0;
+ for (auto it = begin; it != end; ++it, ++i) {
+ int revents = pollfds[i].revents;
+ static constexpr int kAllowedEvents = POLLIN | POLLHUP;
+ DAWN_CHECK((revents & kAllowedEvents) == revents);
+
+ bool ready = (pollfds[i].revents & POLLIN) != 0;
+ *(*it).second = ready;
+ }
+
+ return true;
+#else
+ DAWN_CHECK(false); // Not implemented.
+#endif
+}
+
+} // namespace dawn::native
+
+#endif // SRC_DAWN_NATIVE_WAITANYSYSTEMEVENT_H_
diff --git a/src/dawn/native/metal/DeviceMTL.mm b/src/dawn/native/metal/DeviceMTL.mm
index 30b1645..6453913 100644
--- a/src/dawn/native/metal/DeviceMTL.mm
+++ b/src/dawn/native/metal/DeviceMTL.mm
@@ -34,6 +34,7 @@
#include "dawn/native/ChainUtils_autogen.h"
#include "dawn/native/Commands.h"
#include "dawn/native/ErrorData.h"
+#include "dawn/native/EventManager.h"
#include "dawn/native/metal/BindGroupLayoutMTL.h"
#include "dawn/native/metal/BindGroupMTL.h"
#include "dawn/native/metal/BufferMTL.h"
diff --git a/src/dawn/native/metal/QueueMTL.h b/src/dawn/native/metal/QueueMTL.h
index 1c358fd..63cd463 100644
--- a/src/dawn/native/metal/QueueMTL.h
+++ b/src/dawn/native/metal/QueueMTL.h
@@ -29,9 +29,11 @@
#define SRC_DAWN_NATIVE_METAL_QUEUEMTL_H_
#import <Metal/Metal.h>
+#include <map>
#include "dawn/common/MutexProtected.h"
-#include "dawn/common/SerialQueue.h"
+#include "dawn/common/SerialMap.h"
+#include "dawn/native/EventManager.h"
#include "dawn/native/Queue.h"
#include "dawn/native/SystemEvent.h"
#include "dawn/native/metal/CommandRecordingContext.h"
@@ -50,6 +52,9 @@
void WaitForCommandsToBeScheduled();
void ExportLastSignaledEvent(ExternalImageMTLSharedEventDescriptor* desc);
+ Ref<SystemEvent> CreateWorkDoneSystemEvent(ExecutionSerial serial) override;
+ ResultOrError<bool> WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout) override;
+
private:
Queue(Device* device, const QueueDescriptor* descriptor);
~Queue() override;
@@ -57,7 +62,6 @@
MaybeError Initialize();
void UpdateWaitingEvents(ExecutionSerial completedSerial);
- SystemEventReceiver InsertWorkDoneEvent() override;
MaybeError SubmitImpl(uint32_t commandCount, CommandBufferBase* const* commands) override;
bool HasPendingCommands() const override;
ResultOrError<ExecutionSerial> CheckAndUpdateCompletedSerials() override;
@@ -77,15 +81,16 @@
// different thread, so it needs to be atomic.
std::atomic<uint64_t> mCompletedSerial;
- // A shared event that can be exported for synchronization with other users of Metal.
- // MTLSharedEvent is not available until macOS 10.14+ so use just `id`.
- NSPRef<id> mMtlSharedEvent = nullptr;
-
- // This mutex must be held to access mWaitingEvents (which may happen in a Metal driver thread).
+ // This mutex must be held to access mWaitingEvents (which may happen in a Metal driver
+ // thread).
// TODO(crbug.com/dawn/2065): If we atomically knew a conservative lower bound on the
// mWaitingEvents serials, we could avoid taking this lock sometimes. Optimize if needed.
// See old draft code: https://dawn-review.googlesource.com/c/dawn/+/137502/29
- MutexProtected<SerialQueue<ExecutionSerial, SystemEventPipeSender>> mWaitingEvents;
+ MutexProtected<SerialMap<ExecutionSerial, Ref<SystemEvent>>> mWaitingEvents;
+
+ // A shared event that can be exported for synchronization with other users of Metal.
+ // MTLSharedEvent is not available until macOS 10.14+ so use just `id`.
+ NSPRef<id> mMtlSharedEvent = nullptr;
};
} // namespace dawn::native::metal
diff --git a/src/dawn/native/metal/QueueMTL.mm b/src/dawn/native/metal/QueueMTL.mm
index d351a75..356bc0d 100644
--- a/src/dawn/native/metal/QueueMTL.mm
+++ b/src/dawn/native/metal/QueueMTL.mm
@@ -33,6 +33,7 @@
#include "dawn/native/Commands.h"
#include "dawn/native/DynamicUploader.h"
#include "dawn/native/MetalBackend.h"
+#include "dawn/native/WaitAnySystemEvent.h"
#include "dawn/native/metal/CommandBufferMTL.h"
#include "dawn/native/metal/DeviceMTL.h"
#include "dawn/platform/DawnPlatform.h"
@@ -49,7 +50,7 @@
Queue::Queue(Device* device, const QueueDescriptor* descriptor)
: QueueBase(device, descriptor), mCompletedSerial(0) {}
-Queue::~Queue() {}
+Queue::~Queue() = default;
void Queue::DestroyImpl() {
// Forget all pending commands.
@@ -80,42 +81,14 @@
}
void Queue::UpdateWaitingEvents(ExecutionSerial completedSerial) {
- DAWN_ASSERT(mCompletedSerial >= uint64_t(completedSerial) ||
- completedSerial == kMaxExecutionSerial);
- mWaitingEvents.Use([&](auto waitingEvents) {
- for (auto& waiting : waitingEvents->IterateUpTo(completedSerial)) {
- std::move(waiting).Signal();
+ mWaitingEvents.Use([&](auto events) {
+ for (auto& s : events->IterateUpTo(completedSerial)) {
+ std::move(s)->Signal();
}
- waitingEvents->ClearUpTo(completedSerial);
+ events->ClearUpTo(completedSerial);
});
}
-SystemEventReceiver Queue::InsertWorkDoneEvent() {
- ExecutionSerial serial = GetScheduledWorkDoneSerial();
-
- // TODO(crbug.com/dawn/2051): Optimize to not create a pipe for every WorkDone/MapAsync event.
- // Possible ways to do this:
- // - Don't create the pipe until needed (see the todo on TrackedEvent::mReceiver).
- // - Dedup event pipes when one serial is needed for multiple events (and add a
- // SystemEventReceiver::Duplicate() method which dup()s its underlying pipe receiver).
- // - Create a pipe each for each new serial instead of for each requested event (tradeoff).
- SystemEventPipeSender sender;
- SystemEventReceiver receiver;
- std::tie(sender, receiver) = CreateSystemEventPipe();
-
- mWaitingEvents.Use([&](auto waitingEvents) {
- // Check for device loss while the lock is held. Otherwise, we could enqueue the event
- // after mWaitingEvents has been flushed for device loss, and it'll never get cleaned up.
- if (GetDevice()->IsLost() || mCompletedSerial >= uint64_t(serial)) {
- std::move(sender).Signal();
- } else {
- waitingEvents->Enqueue(std::move(sender), serial);
- }
- });
-
- return receiver;
-}
-
MaybeError Queue::WaitForIdleForDestruction() {
// Forget all pending commands.
mCommandContext.AcquireCommands();
@@ -193,7 +166,7 @@
TRACE_EVENT_ASYNC_END0(platform, GPUWork, "DeviceMTL::SubmitPendingCommandBuffer",
uint64_t(pendingSerial));
DAWN_ASSERT(uint64_t(pendingSerial) > mCompletedSerial.load());
- this->mCompletedSerial = uint64_t(pendingSerial);
+ this->mCompletedSerial.store(uint64_t(pendingSerial), std::memory_order_release);
this->UpdateWaitingEvents(pendingSerial);
}];
@@ -268,4 +241,32 @@
}
}
+Ref<SystemEvent> Queue::CreateWorkDoneSystemEvent(ExecutionSerial serial) {
+ Ref<SystemEvent> completionEvent = AcquireRef(new SystemEvent());
+ mWaitingEvents.Use([&](auto events) {
+ SystemEventReceiver receiver;
+ // Now that we hold the lock, check against mCompletedSerial before inserting.
+ // This serial may have just completed. If it did, mark the event complete.
+ // Also check for device loss. Otherwise, we could enqueue the event
+ // after mWaitingEvents has been flushed for device loss, and it'll never get cleaned up.
+ if (GetDevice()->IsLost() ||
+ serial <= ExecutionSerial(mCompletedSerial.load(std::memory_order_acquire))) {
+ completionEvent->Signal();
+ } else {
+ // Insert the event into the list which will be signaled inside Metal's queue
+ // completion handler.
+ events->Enqueue(completionEvent, serial);
+ }
+ });
+ return completionEvent;
+}
+
+ResultOrError<bool> Queue::WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout) {
+ Ref<SystemEvent> event = CreateWorkDoneSystemEvent(serial);
+ bool ready = false;
+ std::array<std::pair<const dawn::native::SystemEventReceiver&, bool*>, 1> events{
+ {{event->GetOrCreateSystemEventReceiver(), &ready}}};
+ return WaitAnySystemEvent(events.begin(), events.end(), timeout);
+}
+
} // namespace dawn::native::metal
diff --git a/src/dawn/tests/end2end/EventTests.cpp b/src/dawn/tests/end2end/EventTests.cpp
index eb5fa10..2985a3f 100644
--- a/src/dawn/tests/end2end/EventTests.cpp
+++ b/src/dawn/tests/end2end/EventTests.cpp
@@ -208,13 +208,13 @@
switch (GetParam().mWaitTypeAndCallbackMode) {
case WaitTypeAndCallbackMode::TimedWaitAny_WaitAnyOnly:
case WaitTypeAndCallbackMode::TimedWaitAny_AllowSpontaneous:
- return TestWaitImpl(WaitType::TimedWaitAny);
+ return TestWaitImpl(WaitType::TimedWaitAny, loopOnlyOnce);
case WaitTypeAndCallbackMode::SpinWaitAny_WaitAnyOnly:
case WaitTypeAndCallbackMode::SpinWaitAny_AllowSpontaneous:
- return TestWaitImpl(WaitType::SpinWaitAny);
+ return TestWaitImpl(WaitType::SpinWaitAny, loopOnlyOnce);
case WaitTypeAndCallbackMode::SpinProcessEvents_AllowProcessEvents:
case WaitTypeAndCallbackMode::SpinProcessEvents_AllowSpontaneous:
- return TestWaitImpl(WaitType::SpinProcessEvents);
+ return TestWaitImpl(WaitType::SpinProcessEvents, loopOnlyOnce);
}
}
@@ -398,7 +398,7 @@
TrackForTest(f2);
TestWaitAll();
TrackForTest(f1);
- TestWaitAll(true);
+ TestWaitAll(/*loopOnlyOnce=*/true);
}
constexpr WGPUQueueWorkDoneStatus kStatusUninitialized =