blob: 349ee33a644913975bd250ac2f355c7cccd85352 [file]
// Copyright 2026 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 <span>
#include "dawn/wire/Wire.h"
#include "dawn/wire/WireClient.h"
#include "dawn/wire/WireCmd_autogen.h"
#include "dawn/wire/WireServer.h"
#include "src/dawn/common/StringViewUtils.h"
#include "src/dawn/tests/MockCallback.h"
#include "src/dawn/tests/StringViewMatchers.h"
#include "src/dawn/tests/unittests/wire/WireTest.h"
#include "src/dawn/utils/TerribleCommandBuffer.h"
#include "src/dawn/wire/BufferConsumer.h"
#include "src/dawn/wire/ChunkedCommandSerializer.h"
#include "src/dawn/wire/client/Client.h"
#include "src/dawn/wire/server/Server.h"
namespace dawn::wire {
namespace {
using testing::_;
using testing::EmptySizedString;
using testing::InvokeWithoutArgs;
using testing::IsNull;
using testing::MockCppCallback;
using testing::NonEmptySizedString;
using testing::NotNull;
using testing::Return;
using testing::WithArg;
// Fixture that helps execute specific commands through the wire that may not be possible to trigger
// through usage of the dawn::wire::client. It is even more change detecting than regular dawn::wire
// tests so we should use it only when there are no alternatives.
class WireSpecificCommandTests : public WireTest {
protected:
template <typename Cmd>
void AddSpecificServerCmd(const Cmd& cmd) {
CommandSerializer* c2s = GetC2SSerializer();
ChunkedCommandSerializer serializer(c2s);
serializer.SerializeCommand(cmd, *GetWireClient()->GetImplForTesting());
}
// Intercept a command that will be sent from the client to the server. This involves first
// capturing the command, deserializing it as if we were the server, and then re-injecting it
// back into the client command buffer. Note that this does not currently handle object
// serialization correctly, so when modifying commands with descriptors that include other
// WebGPU objects, users may need to manually update those members. This is currently a
// limitation that is hard to address because of the way the commands are [de]serialized in the
// wire.
template <typename Cmd, typename ToIntercept, typename Modifier>
void InterceptServerCmd(ToIntercept toIntercept, Modifier modifier) {
Cmd cmd;
utils::TerribleCommandBuffer* c2sBuf = GetC2SCommandBuffer();
size_t startOffset = c2sBuf->GetOffsetForTesting();
toIntercept();
size_t endOffset = c2sBuf->GetOffsetForTesting();
auto subrange = c2sBuf->GetContentSubrange(startOffset, endOffset);
dawn::wire::DeserializeBuffer deserializeBuffer(subrange);
EXPECT_EQ(WireResult::Success, cmd.Deserialize(&deserializeBuffer, &mAllocator,
*GetWireServer()->GetImplForTesting()));
modifier(&cmd);
c2sBuf->SetOffsetForTesting(startOffset);
AddSpecificServerCmd(cmd);
}
private:
WireDeserializeAllocator mAllocator;
};
// Regression test for https://issues.chromium.org/492139412 where a server receiving
// Device::Destroy wouldn't realize that the buffers got unmapped and would try to write into them.
// While it's not exactly possible to replicate the issue with WireTests since there is no
// dawn::native backend that will unmap buffers on destroy, we can check that the ordering of
// commands in the server is such that it will check that the buffer is mapped before writing into
// it.
TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MappedAtCreation) {
// Create a mapped buffer.
wgpu::BufferDescriptor descriptor = {};
descriptor.size = 4;
descriptor.usage = wgpu::BufferUsage::CopySrc;
descriptor.mappedAtCreation = true;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
WGPUBuffer apiBuffer = api.GetNewBuffer();
EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _))
.WillOnce(Return(apiBuffer))
.RetiresOnSaturation();
FlushClient();
// Force a device destroy without giving the wire::client a chance to unmap client-side buffers.
DeviceDestroyCmd cmd;
cmd.self = device.Get();
AddSpecificServerCmd(cmd);
EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1);
FlushClient();
// A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd
// see a crash here since it would write to nullptr).
EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 0, 4)).WillOnce(Return(nullptr));
EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1);
buffer.Unmap();
FlushClient();
}
// The same test at an offset, to check that it doesn't allow bypassing the null check. It was a
// bug found during review of the fix.
TEST_F(WireSpecificCommandTests, UpdateMappedDataAfterDeviceDestroy_MapWriteOffsetNonZero) {
// Create a mapped buffer.
wgpu::BufferDescriptor descriptor = {};
descriptor.size = 8;
descriptor.usage = wgpu::BufferUsage::MapWrite;
wgpu::Buffer buffer = device.CreateBuffer(&descriptor);
WGPUBuffer apiBuffer = api.GetNewBuffer();
EXPECT_CALL(api, DeviceCreateBuffer(apiDevice, _))
.WillOnce(Return(apiBuffer))
.RetiresOnSaturation();
FlushClient();
// Map the buffer
buffer.MapAsync(wgpu::MapMode::Write, 4, 4, wgpu::CallbackMode::AllowProcessEvents,
[](wgpu::MapAsyncStatus status, wgpu::StringView) {});
EXPECT_CALL(api, OnBufferMapAsync(apiBuffer, WGPUMapMode_Write, 4, 4, _))
.WillOnce(InvokeWithoutArgs([&] {
api.CallBufferMapAsyncCallback(apiBuffer, WGPUMapAsyncStatus_Success,
kEmptyOutputStringView);
}));
FlushClient();
FlushServer();
instance.ProcessEvents();
// Force a device destroy without giving the wire::client a chance to unmap client-side buffers.
DeviceDestroyCmd cmd;
cmd.self = device.Get();
AddSpecificServerCmd(cmd);
EXPECT_CALL(api, DeviceDestroy(apiDevice)).Times(1);
FlushClient();
// A call to unmap will get a nullptr mapped range and should not write to it! (if it were, we'd
// see a crash here since it would write to nullptr).
EXPECT_CALL(api, BufferGetMappedRange(apiBuffer, 4, 4)).WillOnce(Return(nullptr));
EXPECT_CALL(api, BufferUnmap(apiBuffer)).Times(1);
buffer.Unmap();
FlushClient();
}
// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed Device.
TEST_F(WireSpecificCommandTests, RequestDeviceIdReuseAfterInjectedUnregister) {
// Set up all the mock callbacks and objects.
MockCppCallback<wgpu::DeviceLostCallback<void>*> deviceLostCb;
MockCppCallback<wgpu::RequestDeviceCallback<void>*> requestDeviceCb;
WGPUDevice apiDeviceA = api.GetNewDevice();
WGPUFuture futureA;
WGPUDevice apiDeviceB = api.GetNewDevice();
WGPUFuture futureB;
auto SetDeviceCallbacks = [](WGPUDevice apiDevice, const WGPUDeviceDescriptor* desc) {
ProcTableAsClass::Object* object = reinterpret_cast<ProcTableAsClass::Object*>(apiDevice);
object->mDeviceLostCallback = desc->deviceLostCallbackInfo.callback;
object->mDeviceLostUserdata1 = desc->deviceLostCallbackInfo.userdata1;
object->mDeviceLostUserdata2 = desc->deviceLostCallbackInfo.userdata2;
};
// Add the first request for a device and capture the C->S command for it.
wgpu::DeviceDescriptor deviceDescA = {};
deviceDescA.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
deviceLostCb.Callback());
AdapterRequestDeviceCmd requestA = {};
InterceptServerCmd<AdapterRequestDeviceCmd>(
[&]() {
adapter.RequestDevice(&deviceDescA, wgpu::CallbackMode::AllowSpontaneous,
requestDeviceCb.Callback());
},
[&](AdapterRequestDeviceCmd* cmd) { requestA = *cmd; });
// Immediately unregister the device that was reserved for the first request. This is not
// normally possible, so we inject the command directly.
UnregisterObjectCmd unregisterA = {};
unregisterA.objectType = ObjectType::Device;
unregisterA.objectId = requestA.deviceObjectHandle.id;
AddSpecificServerCmd(unregisterA);
// Add a second request for a device that attempts to reuse the same id that was originally
// reserved for the first request.
wgpu::DeviceDescriptor deviceDescB = {};
deviceDescB.SetDeviceLostCallback(wgpu::CallbackMode::AllowSpontaneous,
deviceLostCb.Callback());
AdapterRequestDeviceCmd requestB = {};
InterceptServerCmd<AdapterRequestDeviceCmd>(
[&]() {
adapter.RequestDevice(&deviceDescB, wgpu::CallbackMode::AllowSpontaneous,
requestDeviceCb.Callback());
},
[&](AdapterRequestDeviceCmd* cmd) {
cmd->deviceObjectHandle.id = requestA.deviceObjectHandle.id;
cmd->deviceObjectHandle.generation = requestA.deviceObjectHandle.generation + 1;
requestB = *cmd;
});
EXPECT_CALL(api, OnAdapterRequestDevice(apiAdapter, NotNull(), _))
.WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
SetDeviceCallbacks(apiDeviceA, desc);
futureA = api.GetLastFuture();
}))
.WillOnce(WithArg<1>([&](const WGPUDeviceDescriptor* desc) {
SetDeviceCallbacks(apiDeviceB, desc);
futureB = api.GetLastFuture();
}));
FlushClient();
// Emulate the backend server completing the first request for a device successfully. Even
// though the backend successfully created the device, because the client has already asked to
// unregister that reserved slot, the server should just immediately release the successfully
// created device.
EXPECT_CALL(api, DeviceRelease(apiDeviceA)).Times(1);
api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceA,
kEmptyOutputStringView, futureA);
// Additionally verify that the server does not back the device.
EXPECT_EQ(GetWireServer()->GetDevice(requestA.deviceObjectHandle.id,
requestA.deviceObjectHandle.generation),
nullptr);
// When we flush the server, the RequestDevice callback for the first request should be called
// with callback cancelled, and the DeviceLost callback should also fire once for the first
// requested device.
EXPECT_CALL(requestDeviceCb, Call(wgpu::RequestDeviceStatus::CallbackCancelled, IsNull(),
NonEmptySizedString()));
EXPECT_CALL(deviceLostCb, Call).Times(1);
FlushServer();
// Emulate the backend server completing the second request for a device successfully.
api.CallAdapterRequestDeviceCallback(apiAdapter, WGPURequestDeviceStatus_Success, apiDeviceB,
kEmptyOutputStringView, futureB);
// Verify that the server backs the device.
EXPECT_EQ(GetWireServer()->GetDevice(requestB.deviceObjectHandle.id,
requestB.deviceObjectHandle.generation),
apiDeviceB);
// When the server completes the second request for a device, the RequestDevice callback for
// the second request should be called with success, and the DeviceLost callback should also
// fire once for the second requested device because we don't save the device from the
// RequestDevice callback.
EXPECT_CALL(requestDeviceCb,
Call(wgpu::RequestDeviceStatus::Success, NotNull(), EmptySizedString()));
EXPECT_CALL(deviceLostCb, Call).Times(1);
FlushServer();
// Finally, since we dropped the device on the client side, it should be Unregistered from the
// client, and correspondingly released on the server side once the commands are flushed. Note
// that the client flush should still fail though because we injected a different ID for the
// device than the client frontend actually allocated for the second device. This means that the
// wire client frontend will try to Unregister a non-existent object as a result of the modified
// and injected second request which will cause the server to fail.
EXPECT_CALL(api, DeviceRelease(apiDeviceB)).Times(1);
FlushClient(false);
}
// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed Adapter.
TEST_F(WireSpecificCommandTests, RequestAdapterIdReuseAfterInjectedUnregister) {
// Set up all the mock callbacks and objects.
MockCppCallback<wgpu::RequestAdapterCallback<void>*> requestAdapterCb;
WGPUAdapter apiAdapterA = api.GetNewAdapter();
WGPUFuture futureA;
WGPUAdapter apiAdapterB = api.GetNewAdapter();
WGPUFuture futureB;
// Add the first request for an adapter.
InstanceRequestAdapterCmd requestA = {};
InterceptServerCmd<InstanceRequestAdapterCmd>(
[&]() {
instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
requestAdapterCb.Callback());
},
[&](InstanceRequestAdapterCmd* cmd) { requestA = *cmd; });
// Immediately unregister the adapter that was reserved for the first request. This is not
// normally possible, so we inject the command directly.
UnregisterObjectCmd unregisterA = {};
unregisterA.objectType = ObjectType::Adapter;
unregisterA.objectId = requestA.adapterObjectHandle.id;
AddSpecificServerCmd(unregisterA);
// Add a second request for an adapter that attempts to reuse the same id that was originally
// reserved for the first request.
InstanceRequestAdapterCmd requestB = {};
InterceptServerCmd<InstanceRequestAdapterCmd>(
[&]() {
instance.RequestAdapter(nullptr, wgpu::CallbackMode::AllowSpontaneous,
requestAdapterCb.Callback());
},
[&](InstanceRequestAdapterCmd* cmd) {
cmd->adapterObjectHandle.id = requestA.adapterObjectHandle.id;
cmd->adapterObjectHandle.generation = requestA.adapterObjectHandle.generation + 1;
requestB = *cmd;
});
EXPECT_CALL(api, OnInstanceRequestAdapter(apiInstance, IsNull(), _))
.WillOnce([&]() { futureA = api.GetLastFuture(); })
.WillOnce([&]() { futureB = api.GetLastFuture(); });
FlushClient();
// Emulate the backend server completing the first request for an adapter successfully. Even
// though the backend successfully created the adapter, because the client has already asked to
// unregister that reserved slot, the server should just immediately release the successfully
// created adapter.
EXPECT_CALL(api, AdapterRelease(apiAdapterA)).Times(1);
api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
apiAdapterA, kEmptyOutputStringView, futureA);
// When we flush the server, the RequestAdapter callback for the first request should be called
// with callback cancelled.
EXPECT_CALL(requestAdapterCb, Call(wgpu::RequestAdapterStatus::CallbackCancelled, IsNull(),
NonEmptySizedString()));
FlushServer();
// Emulate the backend server completing the second request for an adapter successfully.
api.CallInstanceRequestAdapterCallback(apiInstance, WGPURequestAdapterStatus_Success,
apiAdapterB, kEmptyOutputStringView, futureB);
// When the server completes the second request for an adapter, the RequestAdapter callback for
// the second request should be called with success.
EXPECT_CALL(requestAdapterCb,
Call(wgpu::RequestAdapterStatus::Success, NotNull(), EmptySizedString()));
FlushServer();
// Finally, since we dropped the adapter on the client side, it should be Unregistered from the
// client, and correspondingly released on the server side once the commands are flushed. Note
// that the client flush should still fail though because we injected a different ID for the
// adapter than the client frontend actually allocated for the second adapter. This means that
// the wire client frontend will try to Unregister a non-existent object as a result of the
// modified and injected second request which will cause the server to fail.
EXPECT_CALL(api, AdapterRelease(apiAdapterB)).Times(1);
FlushClient(false);
}
// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed ComputePipeline. See
// the RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for
// each step throughout the test.
TEST_F(WireSpecificCommandTests, CreateComputePipelineAsyncIdReuseAfterInjectedUnregister) {
wgpu::ShaderModuleDescriptor shaderDesc = {};
wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc);
WGPUShaderModule apiShader = api.GetNewShaderModule();
EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader));
FlushClient();
wgpu::ComputePipelineDescriptor descriptor = {};
descriptor.compute.module = shader;
MockCppCallback<wgpu::CreateComputePipelineAsyncCallback<void>*> createComputePipelineCb;
WGPUComputePipeline apiPipelineA = api.GetNewComputePipeline();
WGPUFuture futureA;
WGPUComputePipeline apiPipelineB = api.GetNewComputePipeline();
WGPUFuture futureB;
DeviceCreateComputePipelineAsyncCmd requestA = {};
InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>(
[&]() {
device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
createComputePipelineCb.Callback());
},
[&](DeviceCreateComputePipelineAsyncCmd* cmd) {
requestA = *cmd;
// Manually fix the shader module since the [de]serialization can't handle objects.
const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module =
shader.Get();
});
UnregisterObjectCmd unregisterA = {};
unregisterA.objectType = ObjectType::ComputePipeline;
unregisterA.objectId = requestA.pipelineObjectHandle.id;
AddSpecificServerCmd(unregisterA);
DeviceCreateComputePipelineAsyncCmd requestB = {};
InterceptServerCmd<DeviceCreateComputePipelineAsyncCmd>(
[&]() {
device.CreateComputePipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
createComputePipelineCb.Callback());
},
[&](DeviceCreateComputePipelineAsyncCmd* cmd) {
cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id;
cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1;
requestB = *cmd;
// Manually fix the shader module since the [de]serialization can't handle objects.
const_cast<WGPUComputePipelineDescriptor*>(cmd->descriptor)->compute.module =
shader.Get();
});
EXPECT_CALL(api, OnDeviceCreateComputePipelineAsync(apiDevice, NotNull(), _))
.WillOnce([&]() { futureA = api.GetLastFuture(); })
.WillOnce([&]() { futureB = api.GetLastFuture(); });
FlushClient();
EXPECT_CALL(api, ComputePipelineRelease(apiPipelineA)).Times(1);
api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice,
WGPUCreatePipelineAsyncStatus_Success,
apiPipelineA, kEmptyOutputStringView, futureA);
EXPECT_CALL(createComputePipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled,
IsNull(), NonEmptySizedString()));
FlushServer();
api.CallDeviceCreateComputePipelineAsyncCallback(apiDevice,
WGPUCreatePipelineAsyncStatus_Success,
apiPipelineB, kEmptyOutputStringView, futureB);
EXPECT_CALL(createComputePipelineCb,
Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString()));
FlushServer();
EXPECT_CALL(api, ComputePipelineRelease(apiPipelineB)).Times(1);
FlushClient(false);
}
// This is a regression test for https://issues.chromium.org/508092644 where a compromised client
// could potentially ask the server to Unregister a reserved, but not backed RenderPipeline. See the
// RequestAdapterIdReuseAfterInjectedUnregister test above for more equivalent descriptions for
// each step throughout the test.
TEST_F(WireSpecificCommandTests, CreateRenderPipelineAsyncIdReuseAfterInjectedUnregister) {
wgpu::ShaderModuleDescriptor shaderDesc = {};
wgpu::ShaderModule shader = device.CreateShaderModule(&shaderDesc);
WGPUShaderModule apiShader = api.GetNewShaderModule();
EXPECT_CALL(api, DeviceCreateShaderModule(apiDevice, _)).WillOnce(Return(apiShader));
FlushClient();
wgpu::FragmentState fragment = {};
fragment.module = shader;
wgpu::RenderPipelineDescriptor descriptor = {};
descriptor.vertex.module = shader;
descriptor.fragment = &fragment;
MockCppCallback<wgpu::CreateRenderPipelineAsyncCallback<void>*> createRenderPipelineCb;
WGPURenderPipeline apiPipelineA = api.GetNewRenderPipeline();
WGPUFuture futureA;
WGPURenderPipeline apiPipelineB = api.GetNewRenderPipeline();
WGPUFuture futureB;
DeviceCreateRenderPipelineAsyncCmd requestA = {};
InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>(
[&]() {
device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
createRenderPipelineCb.Callback());
},
[&](DeviceCreateRenderPipelineAsyncCmd* cmd) {
requestA = *cmd;
// Manually fix the shader module since the [de]serialization can't handle objects.
auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor);
desc->vertex.module = shader.Get();
const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get();
});
UnregisterObjectCmd unregisterA = {};
unregisterA.objectType = ObjectType::RenderPipeline;
unregisterA.objectId = requestA.pipelineObjectHandle.id;
AddSpecificServerCmd(unregisterA);
DeviceCreateRenderPipelineAsyncCmd requestB = {};
InterceptServerCmd<DeviceCreateRenderPipelineAsyncCmd>(
[&]() {
device.CreateRenderPipelineAsync(&descriptor, wgpu::CallbackMode::AllowSpontaneous,
createRenderPipelineCb.Callback());
},
[&](DeviceCreateRenderPipelineAsyncCmd* cmd) {
cmd->pipelineObjectHandle.id = requestA.pipelineObjectHandle.id;
cmd->pipelineObjectHandle.generation = requestA.pipelineObjectHandle.generation + 1;
requestB = *cmd;
// Manually fix the shader module since the [de]serialization can't handle objects.
auto* desc = const_cast<WGPURenderPipelineDescriptor*>(cmd->descriptor);
desc->vertex.module = shader.Get();
const_cast<WGPUFragmentState*>(desc->fragment)->module = shader.Get();
});
EXPECT_CALL(api, OnDeviceCreateRenderPipelineAsync(apiDevice, NotNull(), _))
.WillOnce([&]() { futureA = api.GetLastFuture(); })
.WillOnce([&]() { futureB = api.GetLastFuture(); });
FlushClient();
EXPECT_CALL(api, RenderPipelineRelease(apiPipelineA)).Times(1);
api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice,
WGPUCreatePipelineAsyncStatus_Success,
apiPipelineA, kEmptyOutputStringView, futureA);
EXPECT_CALL(createRenderPipelineCb, Call(wgpu::CreatePipelineAsyncStatus::CallbackCancelled,
IsNull(), NonEmptySizedString()));
FlushServer();
api.CallDeviceCreateRenderPipelineAsyncCallback(apiDevice,
WGPUCreatePipelineAsyncStatus_Success,
apiPipelineB, kEmptyOutputStringView, futureB);
EXPECT_CALL(createRenderPipelineCb,
Call(wgpu::CreatePipelineAsyncStatus::Success, NotNull(), EmptySizedString()));
FlushServer();
EXPECT_CALL(api, RenderPipelineRelease(apiPipelineB)).Times(1);
FlushClient(false);
}
} // anonymous namespace
} // namespace dawn::wire