Implement Queue::OnSubmittedWorkDone

This is the replacement for Fence in the single-queue WebGPU world. To
keep this CL focused, it doesn't deprecate the fences yet.

Bug: chromium:1177476

Change-Id: I09d60732ec67bc1deb49f7a9d57699c049475acf
Reviewed-on: https://dawn-review.googlesource.com/c/dawn/+/41723
Auto-Submit: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: Corentin Wallez <cwallez@chromium.org>
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
diff --git a/src/dawn_native/Queue.cpp b/src/dawn_native/Queue.cpp
index fd65fbd6..53bf294 100644
--- a/src/dawn_native/Queue.cpp
+++ b/src/dawn_native/Queue.cpp
@@ -122,6 +122,27 @@
             return uploadHandle;
         }
 
+        struct SubmittedWorkDone : QueueBase::TaskInFlight {
+            SubmittedWorkDone(WGPUQueueWorkDoneCallback callback, void* userdata)
+                : mCallback(callback), mUserdata(userdata) {
+            }
+            void Finish() override {
+                ASSERT(mCallback != nullptr);
+                mCallback(WGPUQueueWorkDoneStatus_Success, mUserdata);
+                mCallback = nullptr;
+            }
+            void HandleDeviceLoss() override {
+                ASSERT(mCallback != nullptr);
+                mCallback(WGPUQueueWorkDoneStatus_DeviceLost, mUserdata);
+                mCallback = nullptr;
+            }
+            ~SubmittedWorkDone() override = default;
+
+          private:
+            WGPUQueueWorkDoneCallback mCallback = nullptr;
+            void* mUserdata;
+        };
+
         class ErrorQueue : public QueueBase {
           public:
             ErrorQueue(DeviceBase* device) : QueueBase(device, ObjectBase::kError) {
@@ -176,6 +197,26 @@
         fence->UpdateFenceOnComplete(fence, signalValue);
     }
 
+    void QueueBase::OnSubmittedWorkDone(uint64_t signalValue,
+                                        WGPUQueueWorkDoneCallback callback,
+                                        void* userdata) {
+        // The error status depends on the type of error so we let the validation function choose it
+        WGPUQueueWorkDoneStatus status;
+        if (GetDevice()->ConsumedError(ValidateOnSubmittedWorkDone(signalValue, &status))) {
+            callback(status, userdata);
+            return;
+        }
+
+        std::unique_ptr<SubmittedWorkDone> task =
+            std::make_unique<SubmittedWorkDone>(callback, userdata);
+
+        // Technically we only need to wait for previously submitted work but OnSubmittedWorkDone is
+        // also used to make sure ALL queue work is finished in tests, so we also wait for pending
+        // commands (this is non-observable outside of tests so it's ok to do deviate a bit from the
+        // spec).
+        TrackTask(std::move(task), GetDevice()->GetPendingCommandSerial());
+    }
+
     void QueueBase::TrackTask(std::unique_ptr<TaskInFlight> task, ExecutionSerial serial) {
         mTasksInFlight.Enqueue(std::move(task), serial);
         GetDevice()->AddFutureSerial(serial);
@@ -387,6 +428,21 @@
         return {};
     }
 
+    MaybeError QueueBase::ValidateOnSubmittedWorkDone(uint64_t signalValue,
+                                                      WGPUQueueWorkDoneStatus* status) const {
+        *status = WGPUQueueWorkDoneStatus_DeviceLost;
+        DAWN_TRY(GetDevice()->ValidateIsAlive());
+
+        *status = WGPUQueueWorkDoneStatus_Error;
+        DAWN_TRY(GetDevice()->ValidateObject(this));
+
+        if (signalValue != 0) {
+            return DAWN_VALIDATION_ERROR("SignalValue must currently be 0.");
+        }
+
+        return {};
+    }
+
     MaybeError QueueBase::ValidateCreateFence(const FenceDescriptor* descriptor) const {
         DAWN_TRY(GetDevice()->ValidateIsAlive());
         DAWN_TRY(GetDevice()->ValidateObject(this));
diff --git a/src/dawn_native/Queue.h b/src/dawn_native/Queue.h
index a36d912..bdc2007 100644
--- a/src/dawn_native/Queue.h
+++ b/src/dawn_native/Queue.h
@@ -40,6 +40,9 @@
         void Submit(uint32_t commandCount, CommandBufferBase* const* commands);
         void Signal(Fence* fence, uint64_t signalValue);
         Fence* CreateFence(const FenceDescriptor* descriptor);
+        void OnSubmittedWorkDone(uint64_t signalValue,
+                                 WGPUQueueWorkDoneCallback callback,
+                                 void* userdata);
         void WriteBuffer(BufferBase* buffer, uint64_t bufferOffset, const void* data, size_t size);
         void WriteTexture(const TextureCopyView* destination,
                           const void* data,
@@ -87,6 +90,8 @@
 
         MaybeError ValidateSubmit(uint32_t commandCount, CommandBufferBase* const* commands) const;
         MaybeError ValidateSignal(const Fence* fence, FenceAPISerial signalValue) const;
+        MaybeError ValidateOnSubmittedWorkDone(uint64_t signalValue,
+                                               WGPUQueueWorkDoneStatus* status) const;
         MaybeError ValidateCreateFence(const FenceDescriptor* descriptor) const;
         MaybeError ValidateWriteBuffer(const BufferBase* buffer,
                                        uint64_t bufferOffset,
diff --git a/src/dawn_wire/client/ClientDoers.cpp b/src/dawn_wire/client/ClientDoers.cpp
index a5ef7f6..2c6e23f 100644
--- a/src/dawn_wire/client/ClientDoers.cpp
+++ b/src/dawn_wire/client/ClientDoers.cpp
@@ -66,7 +66,6 @@
         if (buffer == nullptr) {
             return true;
         }
-
         return buffer->OnMapAsyncCallback(requestSerial, status, readInitialDataInfoLength,
                                           readInitialDataInfo);
     }
@@ -88,9 +87,17 @@
         if (fence == nullptr) {
             return true;
         }
+        return fence->OnCompletionCallback(requestSerial, status);
+    }
 
-        fence->OnCompletionCallback(requestSerial, status);
-        return true;
+    bool Client::DoQueueWorkDoneCallback(Queue* queue,
+                                         uint64_t requestSerial,
+                                         WGPUQueueWorkDoneStatus status) {
+        // The queue might have been deleted or recreated so this isn't an error.
+        if (queue == nullptr) {
+            return true;
+        }
+        return queue->OnWorkDoneCallback(requestSerial, status);
     }
 
     bool Client::DoDeviceCreateComputePipelineAsyncCallback(Device* device,
diff --git a/src/dawn_wire/client/Fence.cpp b/src/dawn_wire/client/Fence.cpp
index d9a858a..563f13f1 100644
--- a/src/dawn_wire/client/Fence.cpp
+++ b/src/dawn_wire/client/Fence.cpp
@@ -46,7 +46,8 @@
                              WGPUFenceOnCompletionCallback callback,
                              void* userdata) {
         if (client->IsDisconnected()) {
-            return callback(WGPUFenceCompletionStatus_DeviceLost, userdata);
+            callback(WGPUFenceCompletionStatus_DeviceLost, userdata);
+            return;
         }
 
         uint32_t serial = mOnCompletionRequestSerial++;
diff --git a/src/dawn_wire/client/Queue.cpp b/src/dawn_wire/client/Queue.cpp
index f5f68f7..8c9f78b 100644
--- a/src/dawn_wire/client/Queue.cpp
+++ b/src/dawn_wire/client/Queue.cpp
@@ -19,6 +19,47 @@
 
 namespace dawn_wire { namespace client {
 
+    Queue::~Queue() {
+        ClearAllCallbacks(WGPUQueueWorkDoneStatus_Unknown);
+    }
+
+    bool Queue::OnWorkDoneCallback(uint64_t requestSerial, WGPUQueueWorkDoneStatus status) {
+        auto requestIt = mOnWorkDoneRequests.find(requestSerial);
+        if (requestIt == mOnWorkDoneRequests.end()) {
+            return false;
+        }
+
+        // Remove the request data so that the callback cannot be called again.
+        // ex.) inside the callback: if the queue is deleted (when there are multiple queues),
+        // all callbacks reject.
+        OnWorkDoneData request = std::move(requestIt->second);
+        mOnWorkDoneRequests.erase(requestIt);
+
+        request.callback(status, request.userdata);
+        return true;
+    }
+
+    void Queue::OnSubmittedWorkDone(uint64_t signalValue,
+                                    WGPUQueueWorkDoneCallback callback,
+                                    void* userdata) {
+        if (client->IsDisconnected()) {
+            callback(WGPUQueueWorkDoneStatus_DeviceLost, userdata);
+            return;
+        }
+
+        uint32_t serial = mOnWorkDoneSerial++;
+        ASSERT(mOnWorkDoneRequests.find(serial) == mOnWorkDoneRequests.end());
+
+        QueueOnSubmittedWorkDoneCmd cmd;
+        cmd.queueId = this->id;
+        cmd.signalValue = signalValue;
+        cmd.requestSerial = serial;
+
+        mOnWorkDoneRequests[serial] = {callback, userdata};
+
+        client->SerializeCommand(cmd);
+    }
+
     WGPUFence Queue::CreateFence(WGPUFenceDescriptor const* descriptor) {
         auto* allocation = client->FenceAllocator().New(client);
 
@@ -65,4 +106,17 @@
         client->SerializeCommand(cmd);
     }
 
+    void Queue::CancelCallbacksForDisconnect() {
+        ClearAllCallbacks(WGPUQueueWorkDoneStatus_DeviceLost);
+    }
+
+    void Queue::ClearAllCallbacks(WGPUQueueWorkDoneStatus status) {
+        for (auto& it : mOnWorkDoneRequests) {
+            if (it.second.callback) {
+                it.second.callback(status, it.second.userdata);
+            }
+        }
+        mOnWorkDoneRequests.clear();
+    }
+
 }}  // namespace dawn_wire::client
diff --git a/src/dawn_wire/client/Queue.h b/src/dawn_wire/client/Queue.h
index 91c9393..f14fae1 100644
--- a/src/dawn_wire/client/Queue.h
+++ b/src/dawn_wire/client/Queue.h
@@ -27,7 +27,14 @@
     class Queue final : public ObjectBase {
       public:
         using ObjectBase::ObjectBase;
+        ~Queue();
 
+        bool OnWorkDoneCallback(uint64_t requestSerial, WGPUQueueWorkDoneStatus status);
+
+        // Dawn API
+        void OnSubmittedWorkDone(uint64_t signalValue,
+                                 WGPUQueueWorkDoneCallback callback,
+                                 void* userdata);
         WGPUFence CreateFence(const WGPUFenceDescriptor* descriptor);
         void WriteBuffer(WGPUBuffer cBuffer, uint64_t bufferOffset, const void* data, size_t size);
         void WriteTexture(const WGPUTextureCopyView* destination,
@@ -35,6 +42,18 @@
                           size_t dataSize,
                           const WGPUTextureDataLayout* dataLayout,
                           const WGPUExtent3D* writeSize);
+
+      private:
+        void CancelCallbacksForDisconnect() override;
+
+        void ClearAllCallbacks(WGPUQueueWorkDoneStatus status);
+
+        struct OnWorkDoneData {
+            WGPUQueueWorkDoneCallback callback = nullptr;
+            void* userdata = nullptr;
+        };
+        uint64_t mOnWorkDoneSerial = 0;
+        std::map<uint64_t, OnWorkDoneData> mOnWorkDoneRequests;
     };
 
 }}  // namespace dawn_wire::client
diff --git a/src/dawn_wire/server/Server.h b/src/dawn_wire/server/Server.h
index bcedeaa..1124cba 100644
--- a/src/dawn_wire/server/Server.h
+++ b/src/dawn_wire/server/Server.h
@@ -141,6 +141,13 @@
         uint64_t requestSerial;
     };
 
+    struct QueueWorkDoneUserdata : CallbackUserdata {
+        using CallbackUserdata::CallbackUserdata;
+
+        ObjectHandle queue;
+        uint64_t requestSerial;
+    };
+
     struct CreatePipelineAsyncUserData : CallbackUserdata {
         using CallbackUserdata::CallbackUserdata;
 
@@ -202,6 +209,7 @@
                                           FenceCompletionUserdata* userdata);
         void OnFenceOnCompletion(WGPUFenceCompletionStatus status,
                                  FenceOnCompletionUserdata* userdata);
+        void OnQueueWorkDone(WGPUQueueWorkDoneStatus status, QueueWorkDoneUserdata* userdata);
         void OnCreateComputePipelineAsyncCallback(WGPUCreatePipelineAsyncStatus status,
                                                   WGPUComputePipeline pipeline,
                                                   const char* message,
diff --git a/src/dawn_wire/server/ServerQueue.cpp b/src/dawn_wire/server/ServerQueue.cpp
index d798d41..9ab8bc0 100644
--- a/src/dawn_wire/server/ServerQueue.cpp
+++ b/src/dawn_wire/server/ServerQueue.cpp
@@ -40,6 +40,34 @@
         return true;
     }
 
+    void Server::OnQueueWorkDone(WGPUQueueWorkDoneStatus status, QueueWorkDoneUserdata* data) {
+        ReturnQueueWorkDoneCallbackCmd cmd;
+        cmd.queue = data->queue;
+        cmd.requestSerial = data->requestSerial;
+        cmd.status = status;
+
+        SerializeCommand(cmd);
+    }
+
+    bool Server::DoQueueOnSubmittedWorkDone(ObjectId queueId,
+                                            uint64_t signalValue,
+                                            uint64_t requestSerial) {
+        auto* queue = QueueObjects().Get(queueId);
+        if (queue == nullptr) {
+            return false;
+        }
+
+        auto userdata = MakeUserdata<QueueWorkDoneUserdata>();
+        userdata->queue = ObjectHandle{queueId, queue->generation};
+        userdata->requestSerial = requestSerial;
+
+        mProcs.queueOnSubmittedWorkDone(
+            queue->handle, signalValue,
+            ForwardToServer<decltype(&Server::OnQueueWorkDone)>::Func<&Server::OnQueueWorkDone>(),
+            userdata.release());
+        return true;
+    }
+
     bool Server::DoQueueWriteBufferInternal(ObjectId queueId,
                                             ObjectId bufferId,
                                             uint64_t bufferOffset,
diff --git a/src/tests/BUILD.gn b/src/tests/BUILD.gn
index 0c3ffc3..a0baaf2 100644
--- a/src/tests/BUILD.gn
+++ b/src/tests/BUILD.gn
@@ -198,6 +198,7 @@
     "unittests/validation/MinimumBufferSizeValidationTests.cpp",
     "unittests/validation/MultipleDeviceTests.cpp",
     "unittests/validation/QueryValidationTests.cpp",
+    "unittests/validation/QueueOnSubmittedWorkDoneValidationTests.cpp",
     "unittests/validation/QueueSubmitValidationTests.cpp",
     "unittests/validation/QueueWriteBufferValidationTests.cpp",
     "unittests/validation/QueueWriteTextureValidationTests.cpp",
@@ -231,6 +232,7 @@
     "unittests/wire/WireInjectTextureTests.cpp",
     "unittests/wire/WireMemoryTransferServiceTests.cpp",
     "unittests/wire/WireOptionalTests.cpp",
+    "unittests/wire/WireQueueTests.cpp",
     "unittests/wire/WireTest.cpp",
     "unittests/wire/WireTest.h",
     "unittests/wire/WireWGPUDevicePropertiesTests.cpp",
diff --git a/src/tests/end2end/DeviceLostTests.cpp b/src/tests/end2end/DeviceLostTests.cpp
index a15ba94..1e7b539 100644
--- a/src/tests/end2end/DeviceLostTests.cpp
+++ b/src/tests/end2end/DeviceLostTests.cpp
@@ -52,6 +52,16 @@
     mockFenceOnCompletionCallback = nullptr;
 }
 
+class MockQueueWorkDoneCallback {
+  public:
+    MOCK_METHOD(void, Call, (WGPUQueueWorkDoneStatus status, void* userdata));
+};
+
+static std::unique_ptr<MockQueueWorkDoneCallback> mockQueueWorkDoneCallback;
+static void ToMockQueueWorkDone(WGPUQueueWorkDoneStatus status, void* userdata) {
+    mockQueueWorkDoneCallback->Call(status, userdata);
+}
+
 static const int fakeUserData = 0;
 
 class DeviceLostTest : public DawnTest {
@@ -61,11 +71,13 @@
         DAWN_SKIP_TEST_IF(UsesWire());
         mockDeviceLostCallback = std::make_unique<MockDeviceLostCallback>();
         mockFenceOnCompletionCallback = std::make_unique<MockFenceOnCompletionCallback>();
+        mockQueueWorkDoneCallback = std::make_unique<MockQueueWorkDoneCallback>();
     }
 
     void TearDown() override {
         mockDeviceLostCallback = nullptr;
         mockFenceOnCompletionCallback = nullptr;
+        mockQueueWorkDoneCallback = nullptr;
         DawnTest::TearDown();
     }
 
@@ -473,6 +485,28 @@
     EXPECT_EQ(fence.GetCompletedValue(), 2u);
 }
 
+// Test that QueueOnSubmittedWorkDone fails after device is lost.
+TEST_P(DeviceLostTest, QueueOnSubmittedWorkDoneFails) {
+    SetCallbackAndLoseForTesting();
+
+    // callback should have device lost status
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_DeviceLost, nullptr))
+        .Times(1);
+    ASSERT_DEVICE_ERROR(queue.OnSubmittedWorkDone(0, ToMockQueueWorkDone, nullptr));
+    ASSERT_DEVICE_ERROR(device.Tick());
+}
+
+// Test that QueueOnSubmittedWorkDone when the device is lost after calling OnSubmittedWorkDone
+TEST_P(DeviceLostTest, QueueOnSubmittedWorkDoneBeforeLossFails) {
+    // callback should have device lost status
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_DeviceLost, nullptr))
+        .Times(1);
+    queue.OnSubmittedWorkDone(0, ToMockQueueWorkDone, nullptr);
+
+    SetCallbackAndLoseForTesting();
+    ASSERT_DEVICE_ERROR(device.Tick());
+}
+
 // Regression test for the Null backend not properly setting the completedSerial when
 // WaitForIdleForDestruction is called, causing the fence signaling to not be retired and an
 // ASSERT to fire.
diff --git a/src/tests/end2end/QueueTimelineTests.cpp b/src/tests/end2end/QueueTimelineTests.cpp
index d125b18..823a667 100644
--- a/src/tests/end2end/QueueTimelineTests.cpp
+++ b/src/tests/end2end/QueueTimelineTests.cpp
@@ -39,6 +39,16 @@
     mockFenceOnCompletionCallback->Call(status, userdata);
 }
 
+class MockQueueWorkDoneCallback {
+  public:
+    MOCK_METHOD(void, Call, (WGPUQueueWorkDoneStatus status, void* userdata));
+};
+
+static std::unique_ptr<MockQueueWorkDoneCallback> mockQueueWorkDoneCallback;
+static void ToMockQueueWorkDone(WGPUQueueWorkDoneStatus status, void* userdata) {
+    mockQueueWorkDoneCallback->Call(status, userdata);
+}
+
 class QueueTimelineTests : public DawnTest {
   protected:
     void SetUp() override {
@@ -46,6 +56,7 @@
 
         mockMapCallback = std::make_unique<MockMapCallback>();
         mockFenceOnCompletionCallback = std::make_unique<MockFenceOnCompletionCallback>();
+        mockQueueWorkDoneCallback = std::make_unique<MockQueueWorkDoneCallback>();
 
         wgpu::BufferDescriptor descriptor;
         descriptor.size = 4;
@@ -54,8 +65,9 @@
     }
 
     void TearDown() override {
-        mockFenceOnCompletionCallback = nullptr;
         mockMapCallback = nullptr;
+        mockFenceOnCompletionCallback = nullptr;
+        mockQueueWorkDoneCallback = nullptr;
         DawnTest::TearDown();
     }
 
@@ -82,8 +94,24 @@
     mMapReadBuffer.Unmap();
 }
 
+// Test that mMapReadBuffer.MapAsync callback happens before queue.OnWorkDone callback
+// when queue.OnSubmittedWorkDone is called after mMapReadBuffer.MapAsync. The callback order should
+// happen in the order the functions are called.
+TEST_P(QueueTimelineTests, MapRead_OnWorkDone) {
+    testing::InSequence sequence;
+    EXPECT_CALL(*mockMapCallback, Call(WGPUBufferMapAsyncStatus_Success, this)).Times(1);
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, this)).Times(1);
+
+    mMapReadBuffer.MapAsync(wgpu::MapMode::Read, 0, 0, ToMockMapCallback, this);
+
+    queue.OnSubmittedWorkDone(0u, ToMockQueueWorkDone, this);
+
+    WaitForAllOperations();
+    mMapReadBuffer.Unmap();
+}
+
 // Test that fence.OnCompletion callback happens before mMapReadBuffer.MapAsync callback when
-// queue.Signal is called before mMapReadBuffer.MapAsync. The callback order should
+// queue.OnSubmittedWorkDone  is called before mMapReadBuffer.MapAsync. The callback order should
 // happen in the order the functions are called.
 TEST_P(QueueTimelineTests, SignalMapReadOnComplete) {
     testing::InSequence sequence;
@@ -101,6 +129,22 @@
     mMapReadBuffer.Unmap();
 }
 
+// Test that queue.OnWorkDone callback happens before mMapReadBuffer.MapAsync callback when
+// queue.Signal is called before mMapReadBuffer.MapAsync. The callback order should
+// happen in the order the functions are called.
+TEST_P(QueueTimelineTests, OnWorkDone_MapRead) {
+    testing::InSequence sequence;
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, this)).Times(1);
+    EXPECT_CALL(*mockMapCallback, Call(WGPUBufferMapAsyncStatus_Success, this)).Times(1);
+
+    queue.OnSubmittedWorkDone(0u, ToMockQueueWorkDone, this);
+
+    mMapReadBuffer.MapAsync(wgpu::MapMode::Read, 0, 0, ToMockMapCallback, this);
+
+    WaitForAllOperations();
+    mMapReadBuffer.Unmap();
+}
+
 // Test that fence.OnCompletion callback happens before mMapReadBuffer.MapAsync callback when
 // queue.Signal is called before mMapReadBuffer.MapAsync. The callback order should
 // happen in the order the functions are called
@@ -121,6 +165,7 @@
     mMapReadBuffer.Unmap();
 }
 
+// Test a complicated case with many signals surrounding a buffer mapping.
 TEST_P(QueueTimelineTests, SurroundWithFenceSignals) {
     testing::InSequence sequence;
     EXPECT_CALL(*mockFenceOnCompletionCallback, Call(WGPUFenceCompletionStatus_Success, this + 0))
diff --git a/src/tests/unittests/validation/QueueOnSubmittedWorkDoneValidationTests.cpp b/src/tests/unittests/validation/QueueOnSubmittedWorkDoneValidationTests.cpp
new file mode 100644
index 0000000..0b2fc9a
--- /dev/null
+++ b/src/tests/unittests/validation/QueueOnSubmittedWorkDoneValidationTests.cpp
@@ -0,0 +1,58 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/unittests/validation/ValidationTest.h"
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+
+class MockQueueWorkDoneCallback {
+  public:
+    MOCK_METHOD(void, Call, (WGPUQueueWorkDoneStatus status, void* userdata));
+};
+
+static std::unique_ptr<MockQueueWorkDoneCallback> mockQueueWorkDoneCallback;
+static void ToMockQueueWorkDone(WGPUQueueWorkDoneStatus status, void* userdata) {
+    mockQueueWorkDoneCallback->Call(status, userdata);
+}
+
+class QueueOnSubmittedWorkDoneValidationTests : public ValidationTest {
+  protected:
+    void SetUp() override {
+        ValidationTest::SetUp();
+        mockQueueWorkDoneCallback = std::make_unique<MockQueueWorkDoneCallback>();
+    }
+
+    void TearDown() override {
+        mockQueueWorkDoneCallback = nullptr;
+        ValidationTest::TearDown();
+    }
+};
+
+// Test that OnSubmittedWorkDone can be called as soon as the queue is created.
+TEST_F(QueueOnSubmittedWorkDoneValidationTests, CallBeforeSubmits) {
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, this)).Times(1);
+    device.GetQueue().OnSubmittedWorkDone(0u, ToMockQueueWorkDone, this);
+
+    WaitForAllOperations(device);
+}
+
+// Test that OnSubmittedWorkDone is an error if signalValue isn't 0.
+TEST_F(QueueOnSubmittedWorkDoneValidationTests, SignaledValueNotZeroIsInvalid) {
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Error, this)).Times(1);
+    ASSERT_DEVICE_ERROR(device.GetQueue().OnSubmittedWorkDone(1u, ToMockQueueWorkDone, this));
+
+    WaitForAllOperations(device);
+}
diff --git a/src/tests/unittests/wire/WireQueueTests.cpp b/src/tests/unittests/wire/WireQueueTests.cpp
new file mode 100644
index 0000000..0dd340c
--- /dev/null
+++ b/src/tests/unittests/wire/WireQueueTests.cpp
@@ -0,0 +1,99 @@
+// Copyright 2021 The Dawn Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "tests/unittests/wire/WireTest.h"
+
+#include "dawn_wire/WireClient.h"
+
+using namespace testing;
+using namespace dawn_wire;
+
+class MockQueueWorkDoneCallback {
+  public:
+    MOCK_METHOD(void, Call, (WGPUQueueWorkDoneStatus status, void* userdata));
+};
+
+static std::unique_ptr<MockQueueWorkDoneCallback> mockQueueWorkDoneCallback;
+static void ToMockQueueWorkDone(WGPUQueueWorkDoneStatus status, void* userdata) {
+    mockQueueWorkDoneCallback->Call(status, userdata);
+}
+
+class WireQueueTests : public WireTest {
+  protected:
+    void SetUp() override {
+        WireTest::SetUp();
+        mockQueueWorkDoneCallback = std::make_unique<MockQueueWorkDoneCallback>();
+    }
+
+    void TearDown() override {
+        mockQueueWorkDoneCallback = nullptr;
+        WireTest::TearDown();
+    }
+
+    void FlushServer() {
+        WireTest::FlushServer();
+        Mock::VerifyAndClearExpectations(&mockQueueWorkDoneCallback);
+    }
+};
+
+// Test that a successful OnSubmittedWorkDone call is forwarded to the client.
+TEST_F(WireQueueTests, OnSubmittedWorkDoneSuccess) {
+    wgpuQueueOnSubmittedWorkDone(queue, 0u, ToMockQueueWorkDone, this);
+    EXPECT_CALL(api, OnQueueOnSubmittedWorkDone(apiQueue, 0u, _, _))
+        .WillOnce(InvokeWithoutArgs([&]() {
+            api.CallQueueOnSubmittedWorkDoneCallback(apiQueue, WGPUQueueWorkDoneStatus_Success);
+        }));
+    FlushClient();
+
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Success, this)).Times(1);
+    FlushServer();
+}
+
+// Test that an error OnSubmittedWorkDone call is forwarded as an error to the client.
+TEST_F(WireQueueTests, OnSubmittedWorkDoneError) {
+    wgpuQueueOnSubmittedWorkDone(queue, 0u, ToMockQueueWorkDone, this);
+    EXPECT_CALL(api, OnQueueOnSubmittedWorkDone(apiQueue, 0u, _, _))
+        .WillOnce(InvokeWithoutArgs([&]() {
+            api.CallQueueOnSubmittedWorkDoneCallback(apiQueue, WGPUQueueWorkDoneStatus_Error);
+        }));
+    FlushClient();
+
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_Error, this)).Times(1);
+    FlushServer();
+}
+
+// Test registering an OnSubmittedWorkDone then disconnecting the wire calls the callback with
+// device loss
+TEST_F(WireQueueTests, OnSubmittedWorkDoneBeforeDisconnect) {
+    wgpuQueueOnSubmittedWorkDone(queue, 0u, ToMockQueueWorkDone, this);
+    EXPECT_CALL(api, OnQueueOnSubmittedWorkDone(apiQueue, 0u, _, _))
+        .WillOnce(InvokeWithoutArgs([&]() {
+            api.CallQueueOnSubmittedWorkDoneCallback(apiQueue, WGPUQueueWorkDoneStatus_Error);
+        }));
+    FlushClient();
+
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_DeviceLost, this))
+        .Times(1);
+    GetWireClient()->Disconnect();
+}
+
+// Test registering an OnSubmittedWorkDone after disconnecting the wire calls the callback with
+// device loss
+TEST_F(WireQueueTests, OnSubmittedWorkDoneAfterDisconnect) {
+    GetWireClient()->Disconnect();
+
+    EXPECT_CALL(*mockQueueWorkDoneCallback, Call(WGPUQueueWorkDoneStatus_DeviceLost, this))
+        .Times(1);
+    wgpuQueueOnSubmittedWorkDone(queue, 0u, ToMockQueueWorkDone, this);
+}