Reland "[ios blink] Implement futures using WaitListEvent"
This is a reland of commit abc42736ef28ed8fcc11e66814907febff6ee930
The only change over the original is the correct initialization of the
`mSignaled` atomic_bool in WaitListEvent.
Original change's description:
> [ios blink] Implement futures using WaitListEvent
>
> Currently, most futures are implemented using SystemEvent. This includes
> already completed and non-progressing events. This is problematic on iOS
> where BrowserEngineKit child processes are not allowed to open fds, mach
> ports, etc.
>
> This CL introduces WaitListEvent which mimics the base::WaitableEvent
> implementation in Chromium for POSIX platforms. The event internally
> maintains a list of waiters corresponding to a WaitAny call. In WaitAny,
> we create a SyncWaiter that's signaled using a condition variable. The
> waiter is added to each event that the WaitAny is waiting on. The events
> also have a mutex to allow multiple threads to wait on them. We acquire
> the event locks in a globally consistent order (sorted by address) to
> prevent lock order inversion. WaitListEvents can also be waited on
> asynchronously by returning a SystemEventReceiver which allows mixing
> waits on SystemEvents and WaitListEvents.
>
> In addition, this CL changes how already signaled and non-progressing
> TrackedEvents are represented. Already signaled events are backed by
> WaitListEvents and non-progressing events become a flag on TrackedEvent.
>
> The code in EventManager is also cleaned up to aid readability and fix
> an edge case of waiting on multiple queues with zero timeout - we could
> end up ticking the same queue multiple times causing a subtle race
> between MapAsync and OnSubmittedWorkDone futures completion.
>
> Bug: 407801085
> Change-Id: I1c5deb8097339be5beb5e9021d753998a074bea3
> Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/234277
> Reviewed-by: Loko Kung <lokokung@google.com>
> Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
> Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
> Reviewed-by: Kai Ninomiya <kainino@chromium.org>
> Commit-Queue: Kai Ninomiya <kainino@chromium.org>
Bug: 407801085
Change-Id: I254398256851f2310ddfe906c79d389dae1d3d77
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/237719
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Kai Ninomiya <kainino@chromium.org>
Commit-Queue: Sunny Sachanandani <sunnyps@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Kai Ninomiya <kainino@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
diff --git a/src/dawn/native/BUILD.gn b/src/dawn/native/BUILD.gn
index fd055fc..d3bdd62 100644
--- a/src/dawn/native/BUILD.gn
+++ b/src/dawn/native/BUILD.gn
@@ -398,6 +398,8 @@
"ValidationUtils.h",
"VisitableMembers.h",
"WaitAnySystemEvent.h",
+ "WaitListEvent.cpp",
+ "WaitListEvent.h",
"dawn_platform.h",
"stream/BlobSource.cpp",
"stream/BlobSource.h",
diff --git a/src/dawn/native/Buffer.cpp b/src/dawn/native/Buffer.cpp
index f99f976..46aebb1 100644
--- a/src/dawn/native/Buffer.cpp
+++ b/src/dawn/native/Buffer.cpp
@@ -212,7 +212,7 @@
const std::string& message,
WGPUMapAsyncStatus status)
: TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode),
- SystemEvent::CreateSignaled()),
+ TrackedEvent::Completed{}),
mBufferOrError(BufferErrorData{status, message}),
mCallback(callbackInfo.callback),
mUserdata1(callbackInfo.userdata1),
@@ -224,7 +224,7 @@
~MapAsyncEvent() override { EnsureComplete(EventCompletionType::Shutdown); }
void Complete(EventCompletionType completionType) override {
- if (const auto* queueAndSerial = std::get_if<QueueAndSerial>(&GetCompletionData())) {
+ if (const auto* queueAndSerial = GetIfQueueAndSerial()) {
TRACE_EVENT_ASYNC_END0(queueAndSerial->queue->GetDevice()->GetPlatform(), General,
"Buffer::APIMapAsync",
uint64_t(queueAndSerial->completionSerial));
diff --git a/src/dawn/native/CMakeLists.txt b/src/dawn/native/CMakeLists.txt
index 06d7845..2ef2e07 100644
--- a/src/dawn/native/CMakeLists.txt
+++ b/src/dawn/native/CMakeLists.txt
@@ -153,6 +153,7 @@
"ValidationUtils.h"
"VisitableMembers.h"
"WaitAnySystemEvent.h"
+ "WaitListEvent.h"
"webgpu_absl_format.h"
)
@@ -253,6 +254,7 @@
"Toggles.cpp"
"utils/WGPUHelpers.cpp"
"ValidationUtils.cpp"
+ "WaitListEvent.cpp"
"webgpu_absl_format.cpp"
)
diff --git a/src/dawn/native/CreatePipelineAsyncEvent.cpp b/src/dawn/native/CreatePipelineAsyncEvent.cpp
index 844d1d2..3599b45 100644
--- a/src/dawn/native/CreatePipelineAsyncEvent.cpp
+++ b/src/dawn/native/CreatePipelineAsyncEvent.cpp
@@ -41,7 +41,7 @@
#include "dawn/native/EventManager.h"
#include "dawn/native/Instance.h"
#include "dawn/native/RenderPipeline.h"
-#include "dawn/native/SystemEvent.h"
+#include "dawn/native/WaitListEvent.h"
#include "dawn/native/dawn_platform_autogen.h"
#include "dawn/native/utils/WGPUHelpers.h"
#include "dawn/native/wgpu_structs_autogen.h"
@@ -96,8 +96,8 @@
DeviceBase* device,
const CreatePipelineAsyncCallbackInfo& callbackInfo,
Ref<PipelineType> pipeline,
- Ref<SystemEvent> systemEvent)
- : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode), std::move(systemEvent)),
+ Ref<WaitListEvent> event)
+ : TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode), std::move(event)),
mCallback(callbackInfo.callback),
mUserdata1(callbackInfo.userdata1),
mUserdata2(callbackInfo.userdata2),
diff --git a/src/dawn/native/CreatePipelineAsyncEvent.h b/src/dawn/native/CreatePipelineAsyncEvent.h
index b2fefa6..a6bd1e0 100644
--- a/src/dawn/native/CreatePipelineAsyncEvent.h
+++ b/src/dawn/native/CreatePipelineAsyncEvent.h
@@ -56,12 +56,12 @@
public:
using CallbackType = decltype(std::declval<CreatePipelineAsyncCallbackInfo>().callback);
- // Create an event backed by the given system event (for async pipeline creation goes through
+ // Create an event backed by the given wait list event (for async pipeline creation goes through
// the backend).
CreatePipelineAsyncEvent(DeviceBase* device,
const CreatePipelineAsyncCallbackInfo& callbackInfo,
Ref<PipelineType> pipeline,
- Ref<SystemEvent> systemEvent);
+ Ref<WaitListEvent> event);
// Create an event that's ready at creation (for cached results)
CreatePipelineAsyncEvent(DeviceBase* device,
const CreatePipelineAsyncCallbackInfo& callbackInfo,
diff --git a/src/dawn/native/Device.cpp b/src/dawn/native/Device.cpp
index e6f0c63..ce36dc6 100644
--- a/src/dawn/native/Device.cpp
+++ b/src/dawn/native/Device.cpp
@@ -77,6 +77,7 @@
#include "dawn/native/SwapChain.h"
#include "dawn/native/Texture.h"
#include "dawn/native/ValidationUtils_autogen.h"
+#include "dawn/native/WaitListEvent.h"
#include "dawn/native/utils/WGPUHelpers.h"
#include "dawn/platform/DawnPlatform.h"
#include "dawn/platform/metrics/HistogramMacros.h"
@@ -154,7 +155,7 @@
DeviceBase::DeviceLostEvent::DeviceLostEvent(const WGPUDeviceLostCallbackInfo& callbackInfo)
: TrackedEvent(static_cast<wgpu::CallbackMode>(callbackInfo.mode),
- SystemEvent::CreateNonProgressingEvent()),
+ TrackedEvent::NonProgressing{}),
mCallback(callbackInfo.callback),
mUserdata1(callbackInfo.userdata1),
mUserdata2(callbackInfo.userdata2) {}
@@ -1282,7 +1283,7 @@
// New pipeline: create an event backed by system event that is really async.
Ref<CreateComputePipelineAsyncEvent> event = AcquireRef(new CreateComputePipelineAsyncEvent(
this, callbackInfo, std::move(uninitializedComputePipeline),
- AcquireRef(new SystemEvent())));
+ AcquireRef(new WaitListEvent())));
Future future = GetFuture(event);
InitializeComputePipelineAsyncImpl(std::move(event));
return future;
@@ -1350,7 +1351,8 @@
// New pipeline: create an event backed by system event that is really async.
Ref<CreateRenderPipelineAsyncEvent> event = AcquireRef(new CreateRenderPipelineAsyncEvent(
- this, callbackInfo, std::move(uninitializedRenderPipeline), AcquireRef(new SystemEvent())));
+ this, callbackInfo, std::move(uninitializedRenderPipeline),
+ AcquireRef(new WaitListEvent())));
Future future = GetFuture(event);
InitializeRenderPipelineAsyncImpl(std::move(event));
return future;
diff --git a/src/dawn/native/EventManager.cpp b/src/dawn/native/EventManager.cpp
index 835f92c..2780fe2 100644
--- a/src/dawn/native/EventManager.cpp
+++ b/src/dawn/native/EventManager.cpp
@@ -29,6 +29,7 @@
#include <algorithm>
#include <functional>
+#include <type_traits>
#include <utility>
#include <vector>
@@ -58,199 +59,186 @@
bool ready;
};
-// 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 {
+// Wrapper around an iterator to yield event specific objects and a pointer
+// to the ready bool. We pass this into helpers so that they can extract
+// the event specific objects and get pointers to the ready status - without
+// allocating duplicate storage to store the objects and ready bools.
+template <typename Traits>
+class WrappingIterator {
public:
- using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
-
// 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 value_type = typename Traits::value_type;
+ using difference_type = typename Traits::WrappedIter::difference_type;
+ using iterator_category = typename Traits::WrappedIter::iterator_category;
using pointer = value_type*;
using reference = value_type&;
- SystemEventAndReadyStateIterator() = default;
- SystemEventAndReadyStateIterator(const SystemEventAndReadyStateIterator&) = default;
- SystemEventAndReadyStateIterator& operator=(const SystemEventAndReadyStateIterator&) = default;
+ WrappingIterator() = default;
+ WrappingIterator(const WrappingIterator&) = default;
+ WrappingIterator& operator=(const WrappingIterator&) = default;
- explicit SystemEventAndReadyStateIterator(WrappedIter wrappedIt) : mWrappedIt(wrappedIt) {}
+ explicit WrappingIterator(typename Traits::WrappedIter wrappedIt) : mWrappedIt(wrappedIt) {}
- bool operator!=(const SystemEventAndReadyStateIterator& rhs) const {
- return rhs.mWrappedIt != mWrappedIt;
- }
- bool operator==(const SystemEventAndReadyStateIterator& rhs) const {
- return rhs.mWrappedIt == mWrappedIt;
- }
- difference_type operator-(const SystemEventAndReadyStateIterator& rhs) const {
+ bool operator!=(const WrappingIterator& rhs) const { return rhs.mWrappedIt != mWrappedIt; }
+ bool operator==(const WrappingIterator& rhs) const { return rhs.mWrappedIt == mWrappedIt; }
+
+ difference_type operator-(const WrappingIterator& rhs) const {
return mWrappedIt - rhs.mWrappedIt;
}
- SystemEventAndReadyStateIterator operator+(difference_type rhs) const {
- return SystemEventAndReadyStateIterator{mWrappedIt + rhs};
+ WrappingIterator operator+(difference_type rhs) const {
+ return WrappingIterator{mWrappedIt + rhs};
}
- SystemEventAndReadyStateIterator& operator++() {
+ WrappingIterator& operator++() {
++mWrappedIt;
return *this;
}
- value_type operator*() {
- return {
- std::get<Ref<SystemEvent>>(mWrappedIt->event->GetCompletionData())
- ->GetOrCreateSystemEventReceiver(),
- &mWrappedIt->ready,
- };
- }
+ value_type operator*() { return Traits::Deref(mWrappedIt); }
private:
- WrappedIter mWrappedIt;
+ typename Traits::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;
- // TODO(dawn:1662): Make error handling thread-safe.
- auto deviceLock(device->GetScopedLock());
- 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());
- }
- // 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;
+struct ExtractSystemEventAndReadyStateTraits {
+ using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
+ using value_type = std::pair<const SystemEventReceiver&, bool*>;
+
+ static value_type Deref(const WrappedIter& wrappedIt) {
+ if (auto event = wrappedIt->event->GetIfWaitListEvent()) {
+ return {event->WaitAsync(), &wrappedIt->ready};
}
- success = true;
+ DAWN_ASSERT(wrappedIt->event->GetIfSystemEvent());
+ return {
+ wrappedIt->event->GetIfSystemEvent()->GetOrCreateSystemEventReceiver(),
+ &wrappedIt->ready,
+ };
+ }
+};
+
+using SystemEventAndReadyStateIterator = WrappingIterator<ExtractSystemEventAndReadyStateTraits>;
+
+struct ExtractWaitListEventAndReadyStateTraits {
+ using WrappedIter = std::vector<TrackedFutureWaitInfo>::iterator;
+ using value_type = std::pair<Ref<WaitListEvent>, bool*>;
+
+ static value_type Deref(const WrappedIter& wrappedIt) {
+ DAWN_ASSERT(wrappedIt->event->GetIfWaitListEvent());
+ return {wrappedIt->event->GetIfWaitListEvent(), &wrappedIt->ready};
+ }
+};
+
+using WaitListEventAndReadyStateIterator =
+ WrappingIterator<ExtractWaitListEventAndReadyStateTraits>;
+
+// Returns true if at least one future is ready.
+bool PollFutures(std::vector<TrackedFutureWaitInfo>& futures) {
+ bool success = false;
+ for (auto& future : futures) {
+ if (future.event->IsReadyToComplete()) {
+ success = true;
+ future.ready = true;
+ }
}
return success;
}
+// Wait/poll queues with given `timeout`. `queueWaitSerials` should contain per queue, the serial up
+// to which we should flush the queue if needed.
+using QueueWaitSerialsMap = absl::flat_hash_map<QueueBase*, ExecutionSerial>;
+void WaitQueueSerials(const QueueWaitSerialsMap& queueWaitSerials, Nanoseconds timeout) {
+ // TODO(dawn:1662): Make error handling thread-safe.
+ // Poll/wait on queues up to the lowest wait serial, but do this once per queue instead of
+ // per event so that events with same serial complete at the same time instead of racing.
+ for (const auto& queueAndSerial : queueWaitSerials) {
+ auto* queue = queueAndSerial.first;
+ auto waitSerial = queueAndSerial.second;
+
+ auto* device = queue->GetDevice();
+ auto deviceLock(device->GetScopedLock());
+
+ [[maybe_unused]] bool error = device->ConsumedError(
+ [&]() -> MaybeError {
+ if (waitSerial > queue->GetLastSubmittedCommandSerial()) {
+ // Serial has not been submitted yet. Submit it now.
+ DAWN_TRY(queue->EnsureCommandsFlushed(waitSerial));
+ }
+ // Check the completed serial.
+ if (waitSerial > queue->GetCompletedCommandSerial()) {
+ if (timeout > Nanoseconds(0)) {
+ // Wait on the serial if it hasn't passed yet.
+ [[maybe_unused]] bool waitResult = false;
+ DAWN_TRY_ASSIGN(waitResult, queue->WaitForQueueSerial(waitSerial, timeout));
+ }
+ // Update completed serials.
+ DAWN_TRY(queue->CheckPassedSerials());
+ }
+ return {};
+ }(),
+ "waiting for work in %s.", queue);
+ }
+}
+
// We can replace the std::vector& when std::span is available via C++20.
wgpu::WaitStatus WaitImpl(const InstanceBase* instance,
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();
+ bool foundSystemEvent = false;
+ bool foundWaitListEvent = false;
+ QueueWaitSerialsMap queueLowestWaitSerials;
- 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;
+ for (const auto& future : futures) {
+ if (future.event->GetIfSystemEvent()) {
+ foundSystemEvent = true;
}
- // 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.
- instance->EmitLog(WGPULoggingType_Error,
- "Mixed source waits with timeouts are not currently supported.");
- return wgpu::WaitStatus::Error;
+ if (future.event->GetIfWaitListEvent()) {
+ foundWaitListEvent = true;
+ }
+ if (const auto* queueAndSerial = future.event->GetIfQueueAndSerial()) {
+ auto [it, inserted] = queueLowestWaitSerials.insert(
+ {queueAndSerial->queue.Get(), queueAndSerial->completionSerial});
+ if (!inserted) {
+ it->second = std::min(it->second, queueAndSerial->completionSerial);
}
}
-
- 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 != mid; ++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;
+
+ if (timeout == Nanoseconds(0)) {
+ // This is a no-op if `queueLowestWaitSerials` is empty.
+ WaitQueueSerials(queueLowestWaitSerials, timeout);
+ return PollFutures(futures) ? wgpu::WaitStatus::Success : wgpu::WaitStatus::TimedOut;
}
- return wgpu::WaitStatus::Success;
+
+ // We can't have a mix of system/wait-list events and queue-serial events or queue-serial events
+ // from multiple queues with a non-zero timeout.
+ if (queueLowestWaitSerials.size() > 1 ||
+ (!queueLowestWaitSerials.empty() && (foundWaitListEvent || foundSystemEvent))) {
+ // 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.
+ instance->EmitLog(WGPULoggingType_Error,
+ "Mixed source waits with timeouts are not currently supported.");
+ return wgpu::WaitStatus::Error;
+ }
+
+ bool success = false;
+ if (foundSystemEvent) {
+ // Can upgrade wait list events to system events.
+ success = WaitAnySystemEvent(SystemEventAndReadyStateIterator{futures.begin()},
+ SystemEventAndReadyStateIterator{futures.end()}, timeout);
+ } else if (foundWaitListEvent) {
+ success =
+ WaitListEvent::WaitAny(WaitListEventAndReadyStateIterator{futures.begin()},
+ WaitListEventAndReadyStateIterator{futures.end()}, timeout);
+ } else {
+ // This is a no-op if `queueLowestWaitSerials` is empty.
+ WaitQueueSerials(queueLowestWaitSerials, timeout);
+ success = PollFutures(futures);
+ }
+ return success ? wgpu::WaitStatus::Success : wgpu::WaitStatus::TimedOut;
}
// Reorder callbacks to enforce callback ordering required by the spec.
@@ -327,17 +315,7 @@
// Handle the event now if it's spontaneous and ready.
if (event->mCallbackMode == wgpu::CallbackMode::AllowSpontaneous) {
- bool isReady = false;
- auto completionData = event->GetCompletionData();
- if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
- isReady = std::get<Ref<SystemEvent>>(completionData)->IsSignaled();
- }
- if (std::holds_alternative<QueueAndSerial>(completionData)) {
- auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
- isReady = queueAndSerial.completionSerial <=
- queueAndSerial.queue->GetCompletedCommandSerial();
- }
- if (isReady) {
+ if (event->IsReadyToComplete()) {
event->EnsureComplete(EventCompletionType::Ready);
return futureID;
}
@@ -360,15 +338,6 @@
}
void EventManager::SetFutureReady(TrackedEvent* event) {
- auto completionData = event->GetCompletionData();
- if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
- std::get<Ref<SystemEvent>>(completionData)->Signal();
- }
- if (std::holds_alternative<QueueAndSerial>(completionData)) {
- auto& queueAndSerial = std::get<QueueAndSerial>(completionData);
- queueAndSerial.completionSerial = queueAndSerial.queue->GetCompletedCommandSerial();
- }
-
// Sometimes, events might become ready before they are even tracked. This can happen because
// tracking is ordered to uphold callback ordering, but events may become ready in any order. If
// the event is spontaneous, it will be completed when it is tracked.
@@ -376,6 +345,8 @@
return;
}
+ event->SetReadyToComplete();
+
// Handle spontaneous completion now.
if (event->mCallbackMode == wgpu::CallbackMode::AllowSpontaneous) {
mEvents.Use([&](auto events) {
@@ -406,11 +377,7 @@
// Figure out if there are any progressing events. If we only have non-progressing
// events, we need to return false to indicate that there isn't any polling work to
// be done.
- auto completionData = event->GetCompletionData();
- if (std::holds_alternative<Ref<SystemEvent>>(completionData)) {
- hasProgressingEvents |=
- std::get<Ref<SystemEvent>>(completionData)->IsProgressing();
- } else {
+ if (event->IsProgressing()) {
hasProgressingEvents = true;
}
@@ -538,6 +505,10 @@
// EventManager::TrackedEvent
EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
+ Ref<WaitListEvent> completionEvent)
+ : mCallbackMode(callbackMode), mCompletionData(std::move(completionEvent)) {}
+
+EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode,
Ref<SystemEvent> completionEvent)
: mCallbackMode(callbackMode), mCompletionData(std::move(completionEvent)) {}
@@ -547,7 +518,14 @@
: mCallbackMode(callbackMode), mCompletionData(QueueAndSerial{queue, completionSerial}) {}
EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode, Completed tag)
- : TrackedEvent(callbackMode, SystemEvent::CreateSignaled()) {}
+ : mCallbackMode(callbackMode), mCompletionData(AcquireRef(new WaitListEvent())) {
+ GetIfWaitListEvent()->Signal();
+}
+
+EventManager::TrackedEvent::TrackedEvent(wgpu::CallbackMode callbackMode, NonProgressing tag)
+ : mCallbackMode(callbackMode),
+ mCompletionData(AcquireRef(new WaitListEvent())),
+ mIsProgressing(false) {}
EventManager::TrackedEvent::~TrackedEvent() {
DAWN_ASSERT(mFutureID != kNullFutureID);
@@ -558,9 +536,31 @@
return {mFutureID};
}
-const EventManager::TrackedEvent::CompletionData& EventManager::TrackedEvent::GetCompletionData()
- const {
- return mCompletionData;
+bool EventManager::TrackedEvent::IsReadyToComplete() const {
+ bool isReady = false;
+ if (auto event = GetIfSystemEvent()) {
+ isReady = event->IsSignaled();
+ }
+ if (auto event = GetIfWaitListEvent()) {
+ isReady = event->IsSignaled();
+ }
+ if (const auto* queueAndSerial = GetIfQueueAndSerial()) {
+ isReady =
+ queueAndSerial->completionSerial <= queueAndSerial->queue->GetCompletedCommandSerial();
+ }
+ return isReady;
+}
+
+void EventManager::TrackedEvent::SetReadyToComplete() {
+ if (auto event = GetIfSystemEvent()) {
+ event->Signal();
+ }
+ if (auto event = GetIfWaitListEvent()) {
+ event->Signal();
+ }
+ if (auto* queueAndSerial = std::get_if<QueueAndSerial>(&mCompletionData)) {
+ queueAndSerial->completionSerial = queueAndSerial->queue->GetCompletedCommandSerial();
+ }
}
void EventManager::TrackedEvent::EnsureComplete(EventCompletionType completionType) {
diff --git a/src/dawn/native/EventManager.h b/src/dawn/native/EventManager.h
index 6ef872b..a6a4ec0 100644
--- a/src/dawn/native/EventManager.h
+++ b/src/dawn/native/EventManager.h
@@ -43,6 +43,7 @@
#include "dawn/native/Forward.h"
#include "dawn/native/IntegerTypes.h"
#include "dawn/native/SystemEvent.h"
+#include "dawn/native/WaitListEvent.h"
#include "partition_alloc/pointers/raw_ptr.h"
namespace dawn::native {
@@ -116,7 +117,58 @@
// to any TrackedEvents. Any which are not ref'd elsewhere (in order to be `Spontaneous`ly
// completed) will be cleaned up at that time.
class EventManager::TrackedEvent : public RefCounted {
+ public:
+ // Subclasses must implement this to complete the event (if not completed) with
+ // EventCompletionType::Shutdown.
+ ~TrackedEvent() override;
+
+ Future GetFuture() const;
+
+ bool IsProgressing() const { return mIsProgressing; }
+
+ bool IsReadyToComplete() const;
+
+ const QueueAndSerial* GetIfQueueAndSerial() const {
+ return std::get_if<QueueAndSerial>(&mCompletionData);
+ }
+
+ Ref<SystemEvent> GetIfSystemEvent() const {
+ if (auto* event = std::get_if<Ref<SystemEvent>>(&mCompletionData)) {
+ return *event;
+ }
+ return nullptr;
+ }
+
+ Ref<WaitListEvent> GetIfWaitListEvent() const {
+ if (auto* event = std::get_if<Ref<WaitListEvent>>(&mCompletionData)) {
+ return *event;
+ }
+ return nullptr;
+ }
+
+ // Events may be one of three types:
+ // - A queue and the ExecutionSerial after which the event will be completed.
+ // Used for queue completion.
+ // - A SystemEvent which will be signaled usually by the OS / GPU driver. It stores a boolean
+ // that we can check instead of polling with the OS, or it can be transformed lazily into a
+ // SystemEventReceiver.
+ // - A WaitListEvent which will be signaled from our code, usually on a separate thread. It also
+ // stores an atomic boolean that we can check instead of waiting synchronously, or it can be
+ // transformed into a SystemEventReceiver for asynchronous waits.
+ // 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?
protected:
+ friend class EventManager;
+
+ using CompletionData = std::variant<QueueAndSerial, Ref<SystemEvent>, Ref<WaitListEvent>>;
+
+ // Create an event from a WaitListEvent that can be signaled and waited-on in user-space only in
+ // the current process. Note that events like RequestAdapter and RequestDevice complete
+ // immediately in dawn native, and may use an already-completed event.
+ TrackedEvent(wgpu::CallbackMode callbackMode, Ref<WaitListEvent> completionEvent);
+
// Create an event from a SystemEvent. Note that events like RequestAdapter and
// RequestDevice complete immediately in dawn native, and may use an already-completed event.
TrackedEvent(wgpu::CallbackMode callbackMode, Ref<SystemEvent> completionEvent);
@@ -126,33 +178,17 @@
QueueBase* queue,
ExecutionSerial completionSerial);
- struct Completed {};
// Create a TrackedEvent that is already completed.
+ struct Completed {};
TrackedEvent(wgpu::CallbackMode callbackMode, Completed tag);
- public:
- // Subclasses must implement this to complete the event (if not completed) with
- // EventCompletionType::Shutdown.
- ~TrackedEvent() override;
+ // Some SystemEvents may be non-progressing, i.e. DeviceLost. We tag these events so that we can
+ // correctly return whether there is progressing work when users are polling.
+ struct NonProgressing {};
+ TrackedEvent(wgpu::CallbackMode callbackMode, NonProgressing tag);
- Future GetFuture() const;
+ void SetReadyToComplete();
- // 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 CompletionData& GetCompletionData() const;
-
- protected:
void EnsureComplete(EventCompletionType);
virtual void Complete(EventCompletionType) = 0;
@@ -164,9 +200,8 @@
#endif
private:
- friend class EventManager;
-
CompletionData mCompletionData;
+ const bool mIsProgressing = true;
// Callback has been called.
std::atomic<bool> mCompleted = false;
};
diff --git a/src/dawn/native/Queue.cpp b/src/dawn/native/Queue.cpp
index c4f3532..ef30378 100644
--- a/src/dawn/native/Queue.cpp
+++ b/src/dawn/native/Queue.cpp
@@ -55,7 +55,6 @@
#include "dawn/native/QuerySet.h"
#include "dawn/native/RenderPassEncoder.h"
#include "dawn/native/RenderPipeline.h"
-#include "dawn/native/SystemEvent.h"
#include "dawn/native/Texture.h"
#include "dawn/platform/DawnPlatform.h"
#include "dawn/platform/tracing/TraceEvent.h"
diff --git a/src/dawn/native/Queue.h b/src/dawn/native/Queue.h
index bcc3777..1e166a8 100644
--- a/src/dawn/native/Queue.h
+++ b/src/dawn/native/Queue.h
@@ -38,7 +38,6 @@
#include "dawn/native/Forward.h"
#include "dawn/native/IntegerTypes.h"
#include "dawn/native/ObjectBase.h"
-#include "dawn/native/SystemEvent.h"
#include "partition_alloc/pointers/raw_ptr.h"
#include "dawn/native/DawnNative.h"
diff --git a/src/dawn/native/SystemEvent.cpp b/src/dawn/native/SystemEvent.cpp
index f8dd1c8..2b8fe06 100644
--- a/src/dawn/native/SystemEvent.cpp
+++ b/src/dawn/native/SystemEvent.cpp
@@ -52,6 +52,7 @@
SystemEventReceiver::SystemEventReceiver(SystemHandle primitive)
: mPrimitive(std::move(primitive)) {}
+// static
SystemEventReceiver SystemEventReceiver::CreateAlreadySignaled() {
SystemEventPipeSender sender;
SystemEventReceiver receiver;
@@ -131,21 +132,6 @@
// SystemEvent
// static
-Ref<SystemEvent> SystemEvent::CreateSignaled() {
- auto ev = AcquireRef(new SystemEvent());
- ev->Signal();
- return ev;
-}
-
-// static
-Ref<SystemEvent> SystemEvent::CreateNonProgressingEvent() {
- return AcquireRef(new SystemEvent(kNonProgressingPayload));
-}
-
-bool SystemEvent::IsProgressing() const {
- return GetRefCountPayload() != kNonProgressingPayload;
-}
-
bool SystemEvent::IsSignaled() const {
return mSignaled.load(std::memory_order_acquire);
}
diff --git a/src/dawn/native/SystemEvent.h b/src/dawn/native/SystemEvent.h
index 391d430..2a649dc 100644
--- a/src/dawn/native/SystemEvent.h
+++ b/src/dawn/native/SystemEvent.h
@@ -105,12 +105,6 @@
class SystemEvent : public RefCounted {
public:
- using RefCounted::RefCounted;
-
- static Ref<SystemEvent> CreateSignaled();
- static Ref<SystemEvent> CreateNonProgressingEvent();
-
- bool IsProgressing() const;
bool IsSignaled() const;
void Signal();
@@ -119,10 +113,6 @@
const SystemEventReceiver& GetOrCreateSystemEventReceiver();
private:
- // Some SystemEvents may be non-progressing, i.e. DeviceLost. We tag these events so that we can
- // correctly return whether there is progressing work when users are polling.
- static constexpr uint64_t kNonProgressingPayload = 1;
-
// 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.
diff --git a/src/dawn/native/WaitListEvent.cpp b/src/dawn/native/WaitListEvent.cpp
new file mode 100644
index 0000000..8a2f422
--- /dev/null
+++ b/src/dawn/native/WaitListEvent.cpp
@@ -0,0 +1,73 @@
+// Copyright 2025 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.
+
+#include "dawn/native/WaitListEvent.h"
+
+namespace dawn::native {
+
+WaitListEvent::WaitListEvent() = default;
+WaitListEvent::~WaitListEvent() = default;
+
+bool WaitListEvent::IsSignaled() const {
+ return mSignaled.load(std::memory_order_acquire);
+}
+
+void WaitListEvent::Signal() {
+ std::lock_guard<std::mutex> lock(mMutex);
+ DAWN_ASSERT(!mSignaled);
+ mSignaled.store(true, std::memory_order_release);
+ for (SyncWaiter* w : std::move(mSyncWaiters)) {
+ {
+ std::lock_guard<std::mutex> waiterLock(w->mutex);
+ w->waitDone = true;
+ }
+ w->cv.notify_all();
+ }
+ for (auto& sender : std::move(mAsyncWaiters)) {
+ std::move(sender).Signal();
+ }
+}
+
+bool WaitListEvent::Wait(Nanoseconds timeout) {
+ bool ready = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 1> events{{{this, &ready}}};
+ return WaitListEvent::WaitAny(events.begin(), events.end(), timeout);
+}
+
+SystemEventReceiver WaitListEvent::WaitAsync() {
+ std::lock_guard<std::mutex> lock(mMutex);
+ if (IsSignaled()) {
+ return SystemEventReceiver::CreateAlreadySignaled();
+ }
+ SystemEventPipeSender sender;
+ SystemEventReceiver receiver;
+ std::tie(sender, receiver) = CreateSystemEventPipe();
+ mAsyncWaiters.push_back(std::move(sender));
+ return receiver;
+}
+
+} // namespace dawn::native
diff --git a/src/dawn/native/WaitListEvent.h b/src/dawn/native/WaitListEvent.h
new file mode 100644
index 0000000..1addd70
--- /dev/null
+++ b/src/dawn/native/WaitListEvent.h
@@ -0,0 +1,202 @@
+// Copyright 2025 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_WAITLISTEVENT_H_
+#define SRC_DAWN_NATIVE_WAITLISTEVENT_H_
+
+#include <algorithm>
+#include <chrono>
+#include <condition_variable>
+#include <limits>
+#include <mutex>
+#include <utility>
+#include <vector>
+
+#include "dawn/common/RefCounted.h"
+#include "dawn/native/IntegerTypes.h"
+#include "dawn/native/SystemEvent.h"
+#include "partition_alloc/pointers/raw_ptr.h"
+
+namespace dawn::native {
+
+class WaitListEvent : public RefCounted {
+ public:
+ WaitListEvent();
+
+ bool IsSignaled() const;
+ void Signal();
+ bool Wait(Nanoseconds timeout);
+ SystemEventReceiver WaitAsync();
+
+ template <typename It>
+ static bool WaitAny(It eventAndReadyStateBegin, It eventAndReadyStateEnd, Nanoseconds timeout);
+
+ private:
+ ~WaitListEvent() override;
+
+ struct SyncWaiter {
+ std::condition_variable cv;
+ std::mutex mutex;
+ bool waitDone = false;
+ };
+
+ mutable std::mutex mMutex;
+ std::atomic_bool mSignaled{false};
+ std::vector<raw_ptr<SyncWaiter>> mSyncWaiters;
+ std::vector<SystemEventPipeSender> mAsyncWaiters;
+};
+
+template <typename It>
+bool WaitListEvent::WaitAny(It eventAndReadyStateBegin,
+ It eventAndReadyStateEnd,
+ Nanoseconds timeout) {
+ static_assert(std::is_base_of_v<std::random_access_iterator_tag,
+ typename std::iterator_traits<It>::iterator_category>);
+ static_assert(std::is_same_v<typename std::iterator_traits<It>::value_type,
+ std::pair<Ref<WaitListEvent>, bool*>>);
+
+ const size_t count = std::distance(eventAndReadyStateBegin, eventAndReadyStateEnd);
+ if (count == 0) {
+ return false;
+ }
+
+ struct EventState {
+ WaitListEvent* event = nullptr;
+ size_t origIndex;
+ bool isReady = false;
+ };
+ std::vector<EventState> events(count);
+
+ for (size_t i = 0; i < count; i++) {
+ const auto& event = (*(eventAndReadyStateBegin + i)).first;
+ events[i].event = event.Get();
+ events[i].origIndex = i;
+ }
+ // Sort the events by address to get a globally consistent order.
+ std::sort(events.begin(), events.end(),
+ [](const auto& lhs, const auto& rhs) { return lhs.event < rhs.event; });
+
+ // Acquire locks in order and enqueue our waiter.
+ bool foundSignaled = false;
+ for (size_t i = 0; i < count; i++) {
+ WaitListEvent* event = events[i].event;
+ // Skip over multiple waits on the same event, but ensure that we store the same ready state
+ // for duplicates.
+ if (i > 0 && event == events[i - 1].event) {
+ events[i].isReady = events[i - 1].isReady;
+ continue;
+ }
+ event->mMutex.lock();
+ // Check `IsSignaled()` after acquiring the lock so that it doesn't become true immediately
+ // before we acquire the lock - we assume that it is safe to enqueue our waiter after this
+ // point if the event is not already signaled.
+ if (event->IsSignaled()) {
+ events[i].isReady = true;
+ foundSignaled = true;
+ }
+ }
+
+ // If any of the events were already signaled, early out after unlocking the events in reverse
+ // order to prevent lock order inversion.
+ if (foundSignaled) {
+ for (size_t i = 0; i < count; i++) {
+ WaitListEvent* event = events[count - 1 - i].event;
+ // Use the cached value of `IsSignaled()` because we might have unlocked the event
+ // already if it was a duplicate and checking `IsSignaled()` without the lock is racy
+ // and can cause different values of isReady for multiple waits on the same event.
+ if (events[count - 1 - i].isReady) {
+ bool* isReady =
+ (*(eventAndReadyStateBegin + events[count - 1 - i].origIndex)).second;
+ *isReady = true;
+ }
+ // Skip over multiple waits on the same event.
+ if (i > 0 && event == events[count - i].event) {
+ continue;
+ }
+ event->mMutex.unlock();
+ }
+ return true;
+ }
+
+ // We have acquired locks for all the events we're going to wait on - enqueue our waiter now
+ // after locking it since it could be woken up as soon as the event is unlocked and unlock the
+ // events in reverse order now to prevent lock order inversion.
+ SyncWaiter waiter;
+ std::unique_lock<std::mutex> waiterLock(waiter.mutex);
+ for (size_t i = 0; i < count; i++) {
+ WaitListEvent* event = events[count - 1 - i].event;
+ // Skip over multiple waits on the same event.
+ if (i > 0 && event == events[count - i].event) {
+ continue;
+ }
+ event->mSyncWaiters.push_back(&waiter);
+ event->mMutex.unlock();
+ }
+
+ // Any values larger than those representatable by std::chrono::nanoseconds will be treated as
+ // infinite waits - in particular this covers values greater than INT64_MAX.
+ static constexpr uint64_t kMaxDurationNanos = std::chrono::nanoseconds::max().count();
+ if (timeout > Nanoseconds(kMaxDurationNanos)) {
+ waiter.cv.wait(waiterLock, [&waiter]() { return waiter.waitDone; });
+ } else {
+ waiter.cv.wait_for(waiterLock, std::chrono::nanoseconds(static_cast<uint64_t>(timeout)),
+ [&waiter]() { return waiter.waitDone; });
+ }
+
+ // Remove our waiter from the events.
+ for (size_t i = 0; i < count; i++) {
+ WaitListEvent* event = events[i].event;
+ // Skip over multiple waits on the same event, but ensure that we store the same ready state
+ // for duplicates.
+ if (i > 0 && event == events[i - 1].event) {
+ events[i].isReady = events[i - 1].isReady;
+ } else {
+ // We could be woken by the condition variable before the atomic release store to
+ // `mSignaled` is visible - locking the mutex ensures that the atomic acquire load in
+ // `IsSignaled()` sees the correct value.
+ std::lock_guard<std::mutex> eventLock(event->mMutex);
+ if (event->IsSignaled()) {
+ events[i].isReady = true;
+ }
+ event->mSyncWaiters.erase(
+ std::remove(event->mSyncWaiters.begin(), event->mSyncWaiters.end(), &waiter),
+ event->mSyncWaiters.end());
+ }
+ if (events[i].isReady) {
+ bool* isReady = (*(eventAndReadyStateBegin + events[i].origIndex)).second;
+ *isReady = true;
+ foundSignaled = true;
+ }
+ }
+
+ DAWN_ASSERT(!waiter.waitDone || foundSignaled);
+ return foundSignaled;
+}
+
+} // namespace dawn::native
+
+#endif // SRC_DAWN_NATIVE_WAITLISTEVENT_H_
diff --git a/src/dawn/native/metal/QueueMTL.h b/src/dawn/native/metal/QueueMTL.h
index 67a11c2..1e03df3 100644
--- a/src/dawn/native/metal/QueueMTL.h
+++ b/src/dawn/native/metal/QueueMTL.h
@@ -35,7 +35,7 @@
#include "dawn/common/SerialMap.h"
#include "dawn/native/EventManager.h"
#include "dawn/native/Queue.h"
-#include "dawn/native/SystemEvent.h"
+#include "dawn/native/WaitListEvent.h"
#include "dawn/native/metal/CommandRecordingContext.h"
#include "dawn/native/metal/SharedFenceMTL.h"
@@ -53,7 +53,7 @@
id<MTLSharedEvent> GetMTLSharedEvent() const;
ResultOrError<Ref<SharedFence>> GetOrCreateSharedFence();
- Ref<SystemEvent> CreateWorkDoneSystemEvent(ExecutionSerial serial);
+ Ref<WaitListEvent> CreateWorkDoneEvent(ExecutionSerial serial);
ResultOrError<bool> WaitForQueueSerial(ExecutionSerial serial, Nanoseconds timeout) override;
private:
@@ -88,7 +88,7 @@
// 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<SerialMap<ExecutionSerial, Ref<SystemEvent>>> mWaitingEvents;
+ MutexProtected<SerialMap<ExecutionSerial, Ref<WaitListEvent>>> 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`.
diff --git a/src/dawn/native/metal/QueueMTL.mm b/src/dawn/native/metal/QueueMTL.mm
index f1d3f04f..b51248a 100644
--- a/src/dawn/native/metal/QueueMTL.mm
+++ b/src/dawn/native/metal/QueueMTL.mm
@@ -33,7 +33,6 @@
#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"
@@ -252,10 +251,9 @@
}
}
-Ref<SystemEvent> Queue::CreateWorkDoneSystemEvent(ExecutionSerial serial) {
- Ref<SystemEvent> completionEvent = AcquireRef(new SystemEvent());
+Ref<WaitListEvent> Queue::CreateWorkDoneEvent(ExecutionSerial serial) {
+ Ref<WaitListEvent> completionEvent = AcquireRef(new WaitListEvent());
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
@@ -273,11 +271,7 @@
}
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);
+ return CreateWorkDoneEvent(serial)->Wait(timeout);
}
} // namespace dawn::native::metal
diff --git a/src/dawn/tests/BUILD.gn b/src/dawn/tests/BUILD.gn
index cb9b140..05ab8ec 100644
--- a/src/dawn/tests/BUILD.gn
+++ b/src/dawn/tests/BUILD.gn
@@ -780,6 +780,7 @@
"white_box/SharedBufferMemoryTests.h",
"white_box/SharedTextureMemoryTests.cpp",
"white_box/SharedTextureMemoryTests.h",
+ "white_box/WaitListEventTests.cpp",
]
libs = []
diff --git a/src/dawn/tests/white_box/WaitListEventTests.cpp b/src/dawn/tests/white_box/WaitListEventTests.cpp
new file mode 100644
index 0000000..b97c7c9
--- /dev/null
+++ b/src/dawn/tests/white_box/WaitListEventTests.cpp
@@ -0,0 +1,351 @@
+// Copyright 2025 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.
+
+#include <chrono>
+#include <thread>
+#include <utility>
+#include <vector>
+
+#include "dawn/common/Ref.h"
+#include "dawn/native/WaitAnySystemEvent.h"
+#include "dawn/native/WaitListEvent.h"
+#include "dawn/tests/DawnTest.h"
+
+namespace dawn::native {
+namespace {
+
+constexpr uint64_t kZeroDurationNs = 0;
+constexpr uint64_t kShortDurationNs = 1000000;
+constexpr uint64_t kMediumDurationNs = 50000000;
+
+// Helper to wait on a SystemEventReceiver with a timeout
+bool WaitOnReceiver(const SystemEventReceiver& receiver, Nanoseconds timeout) {
+ bool ready = false;
+ std::pair<const SystemEventReceiver&, bool*> event = {receiver, &ready};
+ return WaitAnySystemEvent(&event, &event + 1, timeout);
+}
+
+class WaitListEventTests : public DawnTest {
+ protected:
+ void SetUp() override {
+ DawnTest::SetUp();
+ DAWN_TEST_UNSUPPORTED_IF(UsesWire());
+ }
+};
+
+// Test basic signaling and state checking
+TEST(WaitListEventTests, SignalAndCheck) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ EXPECT_FALSE(event->IsSignaled());
+ event->Signal();
+ EXPECT_TRUE(event->IsSignaled());
+}
+
+// Test waiting on an already signaled event
+TEST(WaitListEventTests, WaitAlreadySignaled) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ event->Signal();
+ EXPECT_TRUE(event->IsSignaled());
+ // Wait with zero timeout should return true immediately
+ EXPECT_TRUE(event->Wait(Nanoseconds(kZeroDurationNs)));
+ // Wait with non-zero timeout should return true immediately
+ EXPECT_TRUE(event->Wait(Nanoseconds(kShortDurationNs)));
+}
+
+// Test waiting on an event that gets signaled later
+TEST(WaitListEventTests, WaitThenSignal) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ EXPECT_FALSE(event->IsSignaled());
+
+ std::thread signaler([&]() {
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event->Signal();
+ });
+
+ // Wait for longer than the signal delay
+ EXPECT_TRUE(event->Wait(Nanoseconds(kMediumDurationNs)));
+ EXPECT_TRUE(event->IsSignaled());
+
+ signaler.join();
+}
+
+// Test waiting with a timeout that expires
+TEST(WaitListEventTests, WaitTimeout) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ EXPECT_FALSE(event->IsSignaled());
+
+ // Wait for a short duration, expect timeout
+ EXPECT_FALSE(event->Wait(Nanoseconds(kShortDurationNs)));
+ EXPECT_FALSE(event->IsSignaled());
+}
+
+// Test waiting with a zero timeout
+TEST(WaitListEventTests, WaitZeroTimeout) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ EXPECT_FALSE(event->IsSignaled());
+ // Wait with zero timeout should return false immediately
+ EXPECT_FALSE(event->Wait(Nanoseconds(kZeroDurationNs)));
+ EXPECT_FALSE(event->IsSignaled());
+
+ event->Signal();
+ EXPECT_TRUE(event->IsSignaled());
+ // Wait with zero timeout should return true immediately
+ EXPECT_TRUE(event->Wait(Nanoseconds(kZeroDurationNs)));
+}
+
+// Test WaitAsync on an already signaled event
+TEST(WaitListEventTests, WaitAsyncAlreadySignaled) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ event->Signal();
+ EXPECT_TRUE(event->IsSignaled());
+
+ SystemEventReceiver receiver = event->WaitAsync();
+ // The receiver should be immediately ready
+ EXPECT_TRUE(WaitOnReceiver(receiver, Nanoseconds(kZeroDurationNs)));
+}
+
+// Test WaitAsync, signaling the event later
+TEST(WaitListEventTests, WaitAsyncThenSignal) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+ EXPECT_FALSE(event->IsSignaled());
+
+ SystemEventReceiver receiver = event->WaitAsync();
+
+ // Check it's not ready yet
+ EXPECT_FALSE(WaitOnReceiver(receiver, Nanoseconds(kZeroDurationNs)));
+
+ std::thread signaler([&]() {
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event->Signal();
+ });
+
+ // Wait for the receiver to become signaled
+ EXPECT_TRUE(WaitOnReceiver(receiver, Nanoseconds(kMediumDurationNs)));
+ EXPECT_TRUE(event->IsSignaled());
+
+ signaler.join();
+}
+
+// Test WaitAny with an empty list
+TEST(WaitListEventTests, WaitAnyEmpty) {
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 0> events;
+ EXPECT_FALSE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
+}
+
+// Test WaitAny where one event is already signaled
+TEST(WaitListEventTests, WaitAnyOneAlreadySignaled) {
+ Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
+ event1->Signal();
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
+ {{event1, &ready1}, {event2, &ready2}}};
+
+ EXPECT_TRUE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
+ EXPECT_TRUE(ready1);
+ EXPECT_FALSE(ready2);
+}
+
+// Test WaitAny where one event is signaled while waiting
+TEST(WaitListEventTests, WaitAnySignalDuringWait) {
+ Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
+ {{event1, &ready1}, {event2, &ready2}}};
+
+ std::thread signaler([&]() {
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event2->Signal(); // Signal the second event
+ });
+
+ EXPECT_TRUE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
+ EXPECT_FALSE(ready1);
+ EXPECT_TRUE(ready2); // Expect the second event to be ready
+
+ signaler.join();
+}
+
+// Test WaitAny with a timeout
+TEST(WaitListEventTests, WaitAnyTimeout) {
+ Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
+ {{event1, &ready1}, {event2, &ready2}}};
+
+ EXPECT_FALSE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kShortDurationNs)));
+ EXPECT_FALSE(ready1);
+ EXPECT_FALSE(ready2);
+}
+
+// Test WaitAny with zero timeout
+TEST(WaitListEventTests, WaitAnyZeroTimeout) {
+ Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 2> events = {
+ {{event1, &ready1}, {event2, &ready2}}};
+
+ // No events signaled
+ EXPECT_FALSE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kZeroDurationNs)));
+ EXPECT_FALSE(ready1);
+ EXPECT_FALSE(ready2);
+
+ // Signal one event
+ event1->Signal();
+ EXPECT_TRUE(WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kZeroDurationNs)));
+ EXPECT_TRUE(ready1);
+ EXPECT_FALSE(ready2);
+}
+
+// Test WaitAny with the same event multiple times
+TEST(WaitListEventTests, WaitAnyDuplicateEvents) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::vector<std::pair<Ref<WaitListEvent>, bool*>> events = {
+ {event, &ready1}, {event, &ready2} // Same event again
+ };
+
+ std::thread signaler([&]() {
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event->Signal();
+ });
+
+ EXPECT_TRUE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
+ // Both ready flags corresponding to the same event should be true
+ EXPECT_TRUE(ready1);
+ EXPECT_TRUE(ready2);
+
+ signaler.join();
+}
+
+// Test WaitAny with the same event multiple times, already signaled
+TEST(WaitListEventTests, WaitAnyDuplicateEventsAlreadySignaled) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ std::vector<std::pair<Ref<WaitListEvent>, bool*>> events = {
+ {event, &ready1}, {event, &ready2} // Same event again
+ };
+
+ // Signal the event *before* waiting
+ event->Signal();
+ EXPECT_TRUE(event->IsSignaled());
+
+ // WaitAny should return immediately since the event is already signaled
+ EXPECT_TRUE(
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs)));
+
+ // Both ready flags corresponding to the same event should be true
+ EXPECT_TRUE(ready1);
+ EXPECT_TRUE(ready2);
+}
+
+// Test multiple threads waiting on the same event
+TEST(WaitListEventTests, WaitMultiThreadedSingleEvent) {
+ Ref<WaitListEvent> event = AcquireRef(new WaitListEvent());
+
+ constexpr size_t kNumWaiters = 5;
+ std::array<std::thread, kNumWaiters> waiters;
+ std::array<std::optional<bool>, kNumWaiters> results;
+
+ for (size_t i = 0; i < kNumWaiters; ++i) {
+ waiters[i] = std::thread(
+ [&results, &event, i]() { results[i] = event->Wait(Nanoseconds(kMediumDurationNs)); });
+ }
+
+ // Give waiters time to start waiting
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event->Signal();
+
+ // Check all waiters returned true
+ for (size_t i = 0; i < kNumWaiters; ++i) {
+ waiters[i].join();
+ EXPECT_TRUE(results[i].has_value());
+ EXPECT_TRUE(results[i].value());
+ }
+ EXPECT_TRUE(event->IsSignaled());
+}
+
+// Test multiple threads waiting on different events via WaitAny
+TEST(WaitListEventTests, WaitAnyMultiThreaded) {
+ Ref<WaitListEvent> event1 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event2 = AcquireRef(new WaitListEvent());
+ Ref<WaitListEvent> event3 = AcquireRef(new WaitListEvent());
+
+ bool ready1 = false;
+ bool ready2 = false;
+ bool ready3 = false;
+ std::array<std::pair<Ref<WaitListEvent>, bool*>, 3> events = {
+ {{event1, &ready1}, {event2, &ready2}, {event3, &ready3}}};
+
+ // Start a thread that waits on any of the events
+ bool waitResult = false;
+ std::thread waiter([&]() {
+ waitResult =
+ WaitListEvent::WaitAny(events.begin(), events.end(), Nanoseconds(kMediumDurationNs));
+ });
+
+ // Start another thread that signals one of the events
+ std::thread signaler([&]() {
+ std::this_thread::sleep_for(std::chrono::nanoseconds(kShortDurationNs));
+ event2->Signal(); // Signal the middle event
+ });
+
+ waiter.join();
+
+ // Check that the waiting thread completes successfully
+ EXPECT_TRUE(waitResult);
+
+ // Check that the correct ready flag was set
+ EXPECT_FALSE(ready1);
+ EXPECT_TRUE(ready2);
+ EXPECT_FALSE(ready3);
+
+ signaler.join();
+}
+
+} // namespace
+} // namespace dawn::native